From 25e348c5ad749693e853eaf12e2c1169fb1d115a Mon Sep 17 00:00:00 2001
From: Scott Erickson
Date: Tue, 12 Jul 2016 14:07:10 -0700
Subject: [PATCH 01/58] Initial, basically working PlayGameDevLevelView
---
app/core/Router.coffee | 3 +-
.../play/level/play-game-dev-level-view.jade | 3 +
.../play/level/PlayGameDevLevelView.coffee | 59 +++++++++++++++++++
3 files changed, 64 insertions(+), 1 deletion(-)
create mode 100644 app/templates/play/level/play-game-dev-level-view.jade
create mode 100644 app/views/play/level/PlayGameDevLevelView.coffee
diff --git a/app/core/Router.coffee b/app/core/Router.coffee
index bec320c7b..4890b9bb5 100644
--- a/app/core/Router.coffee
+++ b/app/core/Router.coffee
@@ -132,6 +132,7 @@ module.exports = class CocoRouter extends Backbone.Router
'play/ladder/:levelID': go('ladder/LadderView')
'play/ladder': go('ladder/MainLadderView')
'play/level/:levelID': go('play/level/PlayLevelView')
+ 'play/game-dev-level/:levelID/:sessionID': go('play/level/PlayGameDevLevelView')
'play/spectate/:levelID': go('play/SpectateView')
'play/:map': go('play/CampaignView')
@@ -192,7 +193,7 @@ module.exports = class CocoRouter extends Backbone.Router
@listenToOnce application.moduleLoader, 'load-complete', ->
@routeDirectly(path, args, options)
return
- return @openView @notFoundView() if not ViewClass
+ return go('NotFoundView') if not ViewClass
view = new ViewClass(options, args...) # options, then any path fragment args
view.render()
@openView(view)
diff --git a/app/templates/play/level/play-game-dev-level-view.jade b/app/templates/play/level/play-game-dev-level-view.jade
new file mode 100644
index 000000000..dd63be30c
--- /dev/null
+++ b/app/templates/play/level/play-game-dev-level-view.jade
@@ -0,0 +1,3 @@
+#canvas-wrapper
+ canvas(width=924, height=589)#webgl-surface
+ canvas(width=924, height=589)#normal-surface
diff --git a/app/views/play/level/PlayGameDevLevelView.coffee b/app/views/play/level/PlayGameDevLevelView.coffee
new file mode 100644
index 000000000..6502c7ee3
--- /dev/null
+++ b/app/views/play/level/PlayGameDevLevelView.coffee
@@ -0,0 +1,59 @@
+RootView = require 'views/core/RootView'
+
+GameUIState = require 'models/GameUIState'
+God = require 'lib/God'
+LevelLoader = require 'lib/LevelLoader'
+GoalManager = require 'lib/world/GoalManager'
+Surface = require 'lib/surface/Surface'
+ThangType = require 'models/ThangType'
+
+module.exports = class PlayGameDevLevelView extends RootView
+ id: 'play-game-dev-level-view'
+ template: require 'templates/play/level/play-game-dev-level-view'
+
+ subscriptions:
+ 'level:started': 'onLevelStarted'
+
+ initialize: (@options, @levelID, @sessionID) ->
+ @gameUIState = new GameUIState()
+ @god = new God({ @gameUIState })
+ @levelLoader = new LevelLoader({ @supermodel, @levelID, @sessionID, observing: true })
+ @listenToOnce @levelLoader, 'world-necessities-loaded', @onWorldNecessitiesLoaded
+ @listenTo @levelLoader, 'world-necessity-load-failed', @onWorldNecessityLoadFailed
+
+ onWorldNecessitiesLoaded: ->
+ { @level, @session, @world, @classMap } = @levelLoader
+ levelObject = @level.serialize(@supermodel, @session)
+ @god.setLevel(levelObject)
+ @god.setWorldClassMap(@classMap)
+ @goalManager = new GoalManager(@world, @level.get('goals'), @team)
+ @god.setGoalManager(@goalManager)
+
+ onWorldNecessityLoadFailed: ->
+ # TODO: handle these and other failures with Promises
+
+ onLoaded: ->
+ _.defer => @onLevelLoaderLoaded()
+
+ onLevelLoaderLoaded: ->
+ return unless @levelLoader.progress() is 1 # double check, since closing the guide may trigger this early
+ @levelLoader.destroy()
+ @levelLoader = null
+ @initSurface()
+
+ initSurface: ->
+ webGLSurface = @$('canvas#webgl-surface')
+ normalSurface = @$('canvas#normal-surface')
+ @surface = new Surface(@world, normalSurface, webGLSurface, {
+ thangTypes: @supermodel.getModels(ThangType)
+ levelType: @level.get('type', true)
+ @gameUIState
+ })
+ worldBounds = @world.getBounds()
+ bounds = [{x: worldBounds.left, y: worldBounds.top}, {x: worldBounds.right, y: worldBounds.bottom}]
+ @surface.camera.setBounds(bounds)
+ @surface.camera.zoomTo({x: 0, y: 0}, 0.1, 0)
+ @surface.setWorld(@world)
+
+ onLevelStarted: ->
+ console.log 'level started'
From b674277e140a7eb0e12a8621fdfa90e0f217e243 Mon Sep 17 00:00:00 2001
From: Scott Erickson
Date: Tue, 12 Jul 2016 14:44:31 -0700
Subject: [PATCH 02/58] Add PlayGameDevLevelView styling
---
.../play/level/play-game-dev-level-view.sass | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
create mode 100644 app/styles/play/level/play-game-dev-level-view.sass
diff --git a/app/styles/play/level/play-game-dev-level-view.sass b/app/styles/play/level/play-game-dev-level-view.sass
new file mode 100644
index 000000000..9b8006bd2
--- /dev/null
+++ b/app/styles/play/level/play-game-dev-level-view.sass
@@ -0,0 +1,19 @@
+#play-game-dev-level-view
+ #canvas-wrapper
+ width: 100%
+ position: relative
+ overflow: hidden
+ z-index: 0
+
+ #webgl-surface
+ background-color: #333
+
+ #normal-surface
+ position: absolute
+ top: 0
+ left: 0
+ pointer-events: none
+
+ canvas#webgl-surface, canvas#normal-surface
+ display: block
+ z-index: 2
From 3a0695f59cef83905a62171e15c215f63336aa80 Mon Sep 17 00:00:00 2001
From: Scott Erickson
Date: Tue, 12 Jul 2016 15:12:11 -0700
Subject: [PATCH 03/58] Add some basic info to PlayGameDevLevelView
---
.../play/level/play-game-dev-level-view.jade | 21 ++++++++++++++++---
.../play/level/PlayGameDevLevelView.coffee | 6 +++++-
2 files changed, 23 insertions(+), 4 deletions(-)
diff --git a/app/templates/play/level/play-game-dev-level-view.jade b/app/templates/play/level/play-game-dev-level-view.jade
index dd63be30c..b20b24b9b 100644
--- a/app/templates/play/level/play-game-dev-level-view.jade
+++ b/app/templates/play/level/play-game-dev-level-view.jade
@@ -1,3 +1,18 @@
-#canvas-wrapper
- canvas(width=924, height=589)#webgl-surface
- canvas(width=924, height=589)#normal-surface
+.container-fluid
+ .row
+ .col-xs-9
+ #canvas-wrapper
+ canvas(width=924, height=589)#webgl-surface
+ canvas(width=924, height=589)#normal-surface
+
+ .col-xs-3#info-col.style-flat
+ h1 Info
+ ul
+ li
+ b Level Name:
+ | #{view.level.get('name')}
+
+ li
+ b Creator:
+ | #{view.session.get('creatorName')}
+
diff --git a/app/views/play/level/PlayGameDevLevelView.coffee b/app/views/play/level/PlayGameDevLevelView.coffee
index 6502c7ee3..fbfcff9a9 100644
--- a/app/views/play/level/PlayGameDevLevelView.coffee
+++ b/app/views/play/level/PlayGameDevLevelView.coffee
@@ -6,6 +6,8 @@ LevelLoader = require 'lib/LevelLoader'
GoalManager = require 'lib/world/GoalManager'
Surface = require 'lib/surface/Surface'
ThangType = require 'models/ThangType'
+Level = require 'models/Level'
+LevelSession = require 'models/LevelSession'
module.exports = class PlayGameDevLevelView extends RootView
id: 'play-game-dev-level-view'
@@ -15,6 +17,8 @@ module.exports = class PlayGameDevLevelView extends RootView
'level:started': 'onLevelStarted'
initialize: (@options, @levelID, @sessionID) ->
+ @level = new Level()
+ @session = new LevelSession()
@gameUIState = new GameUIState()
@god = new God({ @gameUIState })
@levelLoader = new LevelLoader({ @supermodel, @levelID, @sessionID, observing: true })
@@ -56,4 +60,4 @@ module.exports = class PlayGameDevLevelView extends RootView
@surface.setWorld(@world)
onLevelStarted: ->
- console.log 'level started'
+ @renderSelectors '#info-col'
From 1b7ac76b9fcff397b1f722d2a7deb489b77afec3 Mon Sep 17 00:00:00 2001
From: Scott Erickson
Date: Wed, 13 Jul 2016 11:43:25 -0700
Subject: [PATCH 04/58] Add loading and playing to PlayGameDevLevelView
---
app/models/LevelSession.coffee | 13 ++++++++
.../play/level/play-game-dev-level-view.jade | 31 +++++++++++------
app/views/editor/verifier/VerifierTest.coffee | 14 +-------
.../play/level/PlayGameDevLevelView.coffee | 33 +++++++++++++------
4 files changed, 58 insertions(+), 33 deletions(-)
diff --git a/app/models/LevelSession.coffee b/app/models/LevelSession.coffee
index 8b2b73104..72f5a72a6 100644
--- a/app/models/LevelSession.coffee
+++ b/app/models/LevelSession.coffee
@@ -1,4 +1,5 @@
CocoModel = require './CocoModel'
+{createAetherOptions} = require 'lib/aether_utils'
module.exports = class LevelSession extends CocoModel
@className: 'LevelSession'
@@ -93,3 +94,15 @@ module.exports = class LevelSession extends CocoModel
newTopScores.push oldTopScore
state.topScores = newTopScores
@set 'state', state
+
+ generateSpellsObject: ->
+ aetherOptions = createAetherOptions functionName: 'plan', codeLanguage: @get('codeLanguage')
+ spellThang = aether: new Aether aetherOptions
+ spells = "hero-placeholder/plan": thangs: {'Hero Placeholder': spellThang}, name: 'plan'
+ source = @get('code')['hero-placeholder'].plan
+ try
+ spellThang.aether.transpile source
+ catch e
+ console.log "Couldn't transpile!\n#{source}\n", e
+ spellThang.aether.transpile ''
+ spells
diff --git a/app/templates/play/level/play-game-dev-level-view.jade b/app/templates/play/level/play-game-dev-level-view.jade
index b20b24b9b..4b08c5fdf 100644
--- a/app/templates/play/level/play-game-dev-level-view.jade
+++ b/app/templates/play/level/play-game-dev-level-view.jade
@@ -6,13 +6,24 @@
canvas(width=924, height=589)#normal-surface
.col-xs-3#info-col.style-flat
- h1 Info
- ul
- li
- b Level Name:
- | #{view.level.get('name')}
-
- li
- b Creator:
- | #{view.session.get('creatorName')}
-
+ if view.state.get('loading')
+ h1.m-y-1 Loading...
+
+ else
+ h1.m-y-1 Info
+ ul
+ li
+ b Level Name:
+ | #{view.level.get('name')}
+
+ li
+ b Creator:
+ | #{view.session.get('creatorName')}
+
+ - var playing = view.state.get('playing')
+ .m-y-3
+ if playing
+ button#play-btn.btn.btn-lg.btn-burgandy RESTART
+ else
+ button#play-btn.btn.btn-lg.btn-navy PLAY
+
diff --git a/app/views/editor/verifier/VerifierTest.coffee b/app/views/editor/verifier/VerifierTest.coffee
index 7c84e7389..03c6be4a7 100644
--- a/app/views/editor/verifier/VerifierTest.coffee
+++ b/app/views/editor/verifier/VerifierTest.coffee
@@ -78,7 +78,7 @@ module.exports = class VerifierTest extends CocoClass
@listenToOnce @god, 'infinite-loop', @fail
@listenToOnce @god, 'user-code-problem', @onUserCodeProblem
@listenToOnce @god, 'goals-calculated', @processSingleGameResults
- @god.createWorld @generateSpellsObject()
+ @god.createWorld @session.generateSpellsObject()
@updateCallback? state: 'running'
processSingleGameResults: (e) ->
@@ -118,18 +118,6 @@ module.exports = class VerifierTest extends CocoClass
@updateCallback? state: @state
@scheduleCleanup()
- generateSpellsObject: ->
- aetherOptions = createAetherOptions functionName: 'plan', codeLanguage: @session.get('codeLanguage')
- spellThang = aether: new Aether aetherOptions
- spells = "hero-placeholder/plan": thangs: {'Hero Placeholder': spellThang}, name: 'plan'
- source = @session.get('code')['hero-placeholder'].plan
- try
- spellThang.aether.transpile source
- catch e
- console.log "Couldn't transpile!\n#{source}\n", e
- spellThang.aether.transpile ''
- spells
-
scheduleCleanup: ->
setTimeout @cleanup, 100
diff --git a/app/views/play/level/PlayGameDevLevelView.coffee b/app/views/play/level/PlayGameDevLevelView.coffee
index fbfcff9a9..abe85f7d9 100644
--- a/app/views/play/level/PlayGameDevLevelView.coffee
+++ b/app/views/play/level/PlayGameDevLevelView.coffee
@@ -8,30 +8,40 @@ Surface = require 'lib/surface/Surface'
ThangType = require 'models/ThangType'
Level = require 'models/Level'
LevelSession = require 'models/LevelSession'
+{createAetherOptions} = require 'lib/aether_utils'
+State = require 'models/State'
+
+TEAM = 'humans'
module.exports = class PlayGameDevLevelView extends RootView
id: 'play-game-dev-level-view'
template: require 'templates/play/level/play-game-dev-level-view'
-
- subscriptions:
- 'level:started': 'onLevelStarted'
+ events:
+ 'click #play-btn': 'onClickPlayButton'
+
initialize: (@options, @levelID, @sessionID) ->
+ @state = new State({
+ loading: true
+ })
@level = new Level()
@session = new LevelSession()
@gameUIState = new GameUIState()
@god = new God({ @gameUIState })
- @levelLoader = new LevelLoader({ @supermodel, @levelID, @sessionID, observing: true })
+ @levelLoader = new LevelLoader({ @supermodel, @levelID, @sessionID, observing: true, team: TEAM })
@listenToOnce @levelLoader, 'world-necessities-loaded', @onWorldNecessitiesLoaded
@listenTo @levelLoader, 'world-necessity-load-failed', @onWorldNecessityLoadFailed
+ @listenTo @state, 'change', _.debounce(-> @renderSelectors('#info-col'))
onWorldNecessitiesLoaded: ->
{ @level, @session, @world, @classMap } = @levelLoader
levelObject = @level.serialize(@supermodel, @session)
@god.setLevel(levelObject)
- @god.setWorldClassMap(@classMap)
+ @god.setWorldClassMap(@world.classMap)
@goalManager = new GoalManager(@world, @level.get('goals'), @team)
@god.setGoalManager(@goalManager)
+ me.team = TEAM
+ @session.set 'team', TEAM
onWorldNecessityLoadFailed: ->
# TODO: handle these and other failures with Promises
@@ -43,9 +53,6 @@ module.exports = class PlayGameDevLevelView extends RootView
return unless @levelLoader.progress() is 1 # double check, since closing the guide may trigger this early
@levelLoader.destroy()
@levelLoader = null
- @initSurface()
-
- initSurface: ->
webGLSurface = @$('canvas#webgl-surface')
normalSurface = @$('canvas#normal-surface')
@surface = new Surface(@world, normalSurface, webGLSurface, {
@@ -58,6 +65,12 @@ module.exports = class PlayGameDevLevelView extends RootView
@surface.camera.setBounds(bounds)
@surface.camera.zoomTo({x: 0, y: 0}, 0.1, 0)
@surface.setWorld(@world)
-
- onLevelStarted: ->
@renderSelectors '#info-col'
+ @spells = @session.generateSpellsObject()
+ @state.set('loading', false)
+
+ onClickPlayButton: ->
+ @god.createWorld(@spells, false, true)
+ Backbone.Mediator.publish('playback:real-time-playback-started', {})
+ Backbone.Mediator.publish('level:set-playing', {playing: true})
+ @state.set('playing', true)
From d7a2219b16104a4efe17a2e75bcbdb725da0fe62 Mon Sep 17 00:00:00 2001
From: Scott Erickson
Date: Wed, 13 Jul 2016 13:28:54 -0700
Subject: [PATCH 05/58] Refactor PlayGameDevLevelView to use promises
---
app/lib/LevelLoader.coffee | 14 +++-
app/models/SuperModel.coffee | 9 +++
.../play/level/play-game-dev-level-view.jade | 7 +-
.../play/level/PlayGameDevLevelView.coffee | 70 +++++++++----------
4 files changed, 62 insertions(+), 38 deletions(-)
diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee
index 3a1bb39c3..c7d7bc2b5 100644
--- a/app/lib/LevelLoader.coffee
+++ b/app/lib/LevelLoader.coffee
@@ -52,6 +52,15 @@ 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) =>
+ @once 'world-necessities-loaded', => resolve(@)
+ @once 'world-necessity-load-failed', ({resource}) ->
+ { jqxhr } = resource
+ reject({message: jqxhr.responseJSON?.message or jqxhr.responseText or 'Unknown Error'})
+ )
loadLevel: ->
@level = @supermodel.getModel(Level, @levelID) or new Level _id: @levelID
@@ -332,8 +341,8 @@ module.exports = class LevelLoader extends CocoClass
@worldNecessities = (r for r in @worldNecessities when r?)
@onWorldNecessitiesLoaded() if @checkAllWorldNecessitiesRegisteredAndLoaded()
- onWorldNecessityLoadFailed: (resource) ->
- @trigger('world-necessity-load-failed', resource: resource)
+ onWorldNecessityLoadFailed: (event) ->
+ @trigger('world-necessity-load-failed', event)
checkAllWorldNecessitiesRegisteredAndLoaded: ->
return false unless _.filter(@worldNecessities).length is 0
@@ -373,6 +382,7 @@ module.exports = class LevelLoader extends CocoClass
onSupermodelLoaded: ->
return if @destroyed
console.log 'SuperModel for Level loaded in', new Date().getTime() - @t0, 'ms' if LOG
+ console.log 'supermodel loaded'
@loadLevelSounds()
@denormalizeSession()
diff --git a/app/models/SuperModel.coffee b/app/models/SuperModel.coffee
index aca0021bd..eb9fc5b82 100644
--- a/app/models/SuperModel.coffee
+++ b/app/models/SuperModel.coffee
@@ -247,6 +247,15 @@ module.exports = class SuperModel extends Backbone.Model
getResource: (rid) ->
return @resources[rid]
+
+ # Promises
+ finishLoading: ->
+ new Promise (resolve, reject) =>
+ return resolve(@) if @finished()
+ @once 'failed', ({resource}) ->
+ jqxhr = resource.jqxhr
+ reject({message: jqxhr.responseJSON?.message or jqxhr.responseText or 'Unknown Error'})
+ @once 'loaded-all', => resolve(@)
class Resource extends Backbone.Model
constructor: (name, value=1) ->
diff --git a/app/templates/play/level/play-game-dev-level-view.jade b/app/templates/play/level/play-game-dev-level-view.jade
index 4b08c5fdf..928cd1c12 100644
--- a/app/templates/play/level/play-game-dev-level-view.jade
+++ b/app/templates/play/level/play-game-dev-level-view.jade
@@ -6,8 +6,13 @@
canvas(width=924, height=589)#normal-surface
.col-xs-3#info-col.style-flat
- if view.state.get('loading')
+ if view.state.get('errorMessage')
+ .alert.alert-danger= view.state.get('errorMessage')
+
+ else if view.state.get('loading')
h1.m-y-1 Loading...
+ .progress
+ .progress-bar(style="width: #{view.state.get('progress')}")
else
h1.m-y-1 Info
diff --git a/app/views/play/level/PlayGameDevLevelView.coffee b/app/views/play/level/PlayGameDevLevelView.coffee
index abe85f7d9..c9b5bcc9b 100644
--- a/app/views/play/level/PlayGameDevLevelView.coffee
+++ b/app/views/play/level/PlayGameDevLevelView.coffee
@@ -23,51 +23,51 @@ module.exports = class PlayGameDevLevelView extends RootView
initialize: (@options, @levelID, @sessionID) ->
@state = new State({
loading: true
+ progress: 0
})
+
+ @supermodel.on 'update-progress', (progress) =>
+ @state.set({progress: (progress*100).toFixed(1)+'%'})
@level = new Level()
@session = new LevelSession()
@gameUIState = new GameUIState()
@god = new God({ @gameUIState })
@levelLoader = new LevelLoader({ @supermodel, @levelID, @sessionID, observing: true, team: TEAM })
- @listenToOnce @levelLoader, 'world-necessities-loaded', @onWorldNecessitiesLoaded
- @listenTo @levelLoader, 'world-necessity-load-failed', @onWorldNecessityLoadFailed
@listenTo @state, 'change', _.debounce(-> @renderSelectors('#info-col'))
- onWorldNecessitiesLoaded: ->
- { @level, @session, @world, @classMap } = @levelLoader
- levelObject = @level.serialize(@supermodel, @session)
- @god.setLevel(levelObject)
- @god.setWorldClassMap(@world.classMap)
- @goalManager = new GoalManager(@world, @level.get('goals'), @team)
- @god.setGoalManager(@goalManager)
- me.team = TEAM
- @session.set 'team', TEAM
+ @levelLoader.loadWorldNecessities()
- onWorldNecessityLoadFailed: ->
- # TODO: handle these and other failures with Promises
+ .then (levelLoader) => # grabbing from the levelLoader
+ { @level, @session, @world } = levelLoader
+ @god.setLevel(@level.serialize(@supermodel, @session))
+ @god.setWorldClassMap(@world.classMap)
+ @goalManager = new GoalManager(@world, @level.get('goals'), @team)
+ @god.setGoalManager(@goalManager)
+ me.team = TEAM
+ @session.set 'team', TEAM
+ return @supermodel.finishLoading()
+
+ .then (supermodel) =>
+ @levelLoader.destroy()
+ @levelLoader = null
+ webGLSurface = @$('canvas#webgl-surface')
+ normalSurface = @$('canvas#normal-surface')
+ @surface = new Surface(@world, normalSurface, webGLSurface, {
+ thangTypes: @supermodel.getModels(ThangType)
+ levelType: @level.get('type', true)
+ @gameUIState
+ })
+ worldBounds = @world.getBounds()
+ bounds = [{x: worldBounds.left, y: worldBounds.top}, {x: worldBounds.right, y: worldBounds.bottom}]
+ @surface.camera.setBounds(bounds)
+ @surface.camera.zoomTo({x: 0, y: 0}, 0.1, 0)
+ @surface.setWorld(@world)
+ @renderSelectors '#info-col'
+ @spells = @session.generateSpellsObject()
+ @state.set('loading', false)
- onLoaded: ->
- _.defer => @onLevelLoaderLoaded()
-
- onLevelLoaderLoaded: ->
- return unless @levelLoader.progress() is 1 # double check, since closing the guide may trigger this early
- @levelLoader.destroy()
- @levelLoader = null
- webGLSurface = @$('canvas#webgl-surface')
- normalSurface = @$('canvas#normal-surface')
- @surface = new Surface(@world, normalSurface, webGLSurface, {
- thangTypes: @supermodel.getModels(ThangType)
- levelType: @level.get('type', true)
- @gameUIState
- })
- worldBounds = @world.getBounds()
- bounds = [{x: worldBounds.left, y: worldBounds.top}, {x: worldBounds.right, y: worldBounds.bottom}]
- @surface.camera.setBounds(bounds)
- @surface.camera.zoomTo({x: 0, y: 0}, 0.1, 0)
- @surface.setWorld(@world)
- @renderSelectors '#info-col'
- @spells = @session.generateSpellsObject()
- @state.set('loading', false)
+ .catch ({message}) =>
+ @state.set('errorMessage', message)
onClickPlayButton: ->
@god.createWorld(@spells, false, true)
From f88223b994c3769d381f68d289ba1aec59b18378 Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Wed, 13 Jul 2016 14:20:22 -0700
Subject: [PATCH 06/58] Fix spawning Hero kind ThangTypes in game-dev levels
---
app/models/Level.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/models/Level.coffee b/app/models/Level.coffee
index 8df696750..ab02889d3 100644
--- a/app/models/Level.coffee
+++ b/app/models/Level.coffee
@@ -33,7 +33,7 @@ module.exports = class Level extends CocoModel
for tt in supermodel.getModels ThangType
if tmap[tt.get('original')] or
(tt.get('kind') isnt 'Hero' and tt.get('kind')? and tt.get('components') and not tt.notInLevel) or
- (tt.get('kind') is 'Hero' and ((@get('type', true) in ['course', 'course-ladder']) or tt.get('original') in sessionHeroes))
+ (tt.get('kind') is 'Hero' and ((@get('type', true) in ['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
From 45c8c2006de7865141b08b2f5da3ff9fb5d1cacb Mon Sep 17 00:00:00 2001
From: Scott Erickson
Date: Wed, 13 Jul 2016 13:46:03 -0700
Subject: [PATCH 07/58] Quick fix LevelSessions require error
In areas of the site that do not have lib/aether_utils, the require broke because it's
fetched only sometimes through the ModuleLoader.
---
app/models/LevelSession.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/models/LevelSession.coffee b/app/models/LevelSession.coffee
index 72f5a72a6..1d4334535 100644
--- a/app/models/LevelSession.coffee
+++ b/app/models/LevelSession.coffee
@@ -1,5 +1,4 @@
CocoModel = require './CocoModel'
-{createAetherOptions} = require 'lib/aether_utils'
module.exports = class LevelSession extends CocoModel
@className: 'LevelSession'
@@ -96,6 +95,7 @@ module.exports = class LevelSession extends CocoModel
@set 'state', state
generateSpellsObject: ->
+ {createAetherOptions} = require 'lib/aether_utils'
aetherOptions = createAetherOptions functionName: 'plan', codeLanguage: @get('codeLanguage')
spellThang = aether: new Aether aetherOptions
spells = "hero-placeholder/plan": thangs: {'Hero Placeholder': spellThang}, name: 'plan'
From c9986ee05a96508ad2cd4280a7af11b8345a1488 Mon Sep 17 00:00:00 2001
From: Scott Erickson
Date: Wed, 13 Jul 2016 13:52:22 -0700
Subject: [PATCH 08/58] Tweak Promises in PlayGameDevLevelView
---
app/lib/LevelLoader.coffee | 2 +-
app/views/play/level/PlayGameDevLevelView.coffee | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee
index c7d7bc2b5..6a15df091 100644
--- a/app/lib/LevelLoader.coffee
+++ b/app/lib/LevelLoader.coffee
@@ -56,6 +56,7 @@ module.exports = class LevelLoader extends CocoClass
loadWorldNecessities: ->
# TODO: Actually trigger loading, instead of in the constructor
new Promise((resolve, reject) =>
+ return resolve(@) if @world
@once 'world-necessities-loaded', => resolve(@)
@once 'world-necessity-load-failed', ({resource}) ->
{ jqxhr } = resource
@@ -382,7 +383,6 @@ module.exports = class LevelLoader extends CocoClass
onSupermodelLoaded: ->
return if @destroyed
console.log 'SuperModel for Level loaded in', new Date().getTime() - @t0, 'ms' if LOG
- console.log 'supermodel loaded'
@loadLevelSounds()
@denormalizeSession()
diff --git a/app/views/play/level/PlayGameDevLevelView.coffee b/app/views/play/level/PlayGameDevLevelView.coffee
index c9b5bcc9b..78d18e8f8 100644
--- a/app/views/play/level/PlayGameDevLevelView.coffee
+++ b/app/views/play/level/PlayGameDevLevelView.coffee
@@ -37,7 +37,7 @@ module.exports = class PlayGameDevLevelView extends RootView
@levelLoader.loadWorldNecessities()
- .then (levelLoader) => # grabbing from the levelLoader
+ .then (levelLoader) =>
{ @level, @session, @world } = levelLoader
@god.setLevel(@level.serialize(@supermodel, @session))
@god.setWorldClassMap(@world.classMap)
@@ -45,7 +45,7 @@ module.exports = class PlayGameDevLevelView extends RootView
@god.setGoalManager(@goalManager)
me.team = TEAM
@session.set 'team', TEAM
- return @supermodel.finishLoading()
+ @supermodel.finishLoading()
.then (supermodel) =>
@levelLoader.destroy()
From 4a51045a417cf08955659d5589e26c9ce16ea904 Mon Sep 17 00:00:00 2001
From: Scott Erickson
Date: Wed, 13 Jul 2016 15:45:06 -0700
Subject: [PATCH 09/58] Fix PlayGameDevLevelView when playing the first time,
get frames streaming
For whatever reason, the Angel does not normally allow streaming on the first world.
I hacked around it, but would be good to figure out why that restriction is there
in the first place.
---
app/lib/God.coffee | 9 +++++----
app/views/play/level/PlayGameDevLevelView.coffee | 1 +
2 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/app/lib/God.coffee b/app/lib/God.coffee
index 1371958f7..272213028 100644
--- a/app/lib/God.coffee
+++ b/app/lib/God.coffee
@@ -94,9 +94,9 @@ module.exports = class God extends CocoClass
return if hadPreloader
@angelsShare.workQueue = []
- work =
+ work = {
userCodeMap: userCodeMap
- level: @level
+ @level
levelSessionIDs: @levelSessionIDs
submissionCount: @lastSubmissionCount
fixedSeed: @lastFixedSeed
@@ -104,9 +104,10 @@ module.exports = class God extends CocoClass
difficulty: @lastDifficulty
goals: @angelsShare.goalManager?.getGoals()
headless: @angelsShare.headless
- preload: preload
+ preload
synchronous: not Worker? # Profiling world simulation is easier on main thread, or we are IE9.
- realTime: realTime
+ realTime
+ }
@angelsShare.workQueue.push work
angel.workIfIdle() for angel in @angelsShare.angels
work
diff --git a/app/views/play/level/PlayGameDevLevelView.coffee b/app/views/play/level/PlayGameDevLevelView.coffee
index 78d18e8f8..cde0b0c1b 100644
--- a/app/views/play/level/PlayGameDevLevelView.coffee
+++ b/app/views/play/level/PlayGameDevLevelView.coffee
@@ -43,6 +43,7 @@ module.exports = class PlayGameDevLevelView extends RootView
@god.setWorldClassMap(@world.classMap)
@goalManager = new GoalManager(@world, @level.get('goals'), @team)
@god.setGoalManager(@goalManager)
+ @god.angelsShare.firstWorld = false # HACK
me.team = TEAM
@session.set 'team', TEAM
@supermodel.finishLoading()
From b982f3fd5269c188751d70641f54436a53035722 Mon Sep 17 00:00:00 2001
From: Scott Erickson
Date: Wed, 13 Jul 2016 16:04:44 -0700
Subject: [PATCH 10/58] Fix Camera bounds by adding ScriptManager
---
app/views/play/level/PlayGameDevLevelView.coffee | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/app/views/play/level/PlayGameDevLevelView.coffee b/app/views/play/level/PlayGameDevLevelView.coffee
index cde0b0c1b..ab7e2fb9e 100644
--- a/app/views/play/level/PlayGameDevLevelView.coffee
+++ b/app/views/play/level/PlayGameDevLevelView.coffee
@@ -4,6 +4,7 @@ GameUIState = require 'models/GameUIState'
God = require 'lib/God'
LevelLoader = require 'lib/LevelLoader'
GoalManager = require 'lib/world/GoalManager'
+ScriptManager = require 'lib/scripts/ScriptManager'
Surface = require 'lib/surface/Surface'
ThangType = require 'models/ThangType'
Level = require 'models/Level'
@@ -46,6 +47,9 @@ module.exports = class PlayGameDevLevelView extends RootView
@god.angelsShare.firstWorld = false # HACK
me.team = TEAM
@session.set 'team', TEAM
+ @scriptManager = new ScriptManager({
+ scripts: @world.scripts or [], view: @, @session, levelID: @level.get('slug')})
+ @scriptManager.loadFromSession() # Should we? TODO: Figure out how scripts work for game dev levels
@supermodel.finishLoading()
.then (supermodel) =>
@@ -63,6 +67,7 @@ module.exports = class PlayGameDevLevelView extends RootView
@surface.camera.setBounds(bounds)
@surface.camera.zoomTo({x: 0, y: 0}, 0.1, 0)
@surface.setWorld(@world)
+ @scriptManager.initializeCamera()
@renderSelectors '#info-col'
@spells = @session.generateSpellsObject()
@state.set('loading', false)
From fb9998b15e21b070cc8d73c469c84356f4b7a59e Mon Sep 17 00:00:00 2001
From: Scott Erickson
Date: Wed, 13 Jul 2016 16:05:35 -0700
Subject: [PATCH 11/58] Add temp buttons to CourseDetailsView for testing
PlayGameDevLevelView
---
app/templates/courses/course-details.jade | 5 +++++
app/views/courses/CourseDetailsView.coffee | 1 +
2 files changed, 6 insertions(+)
diff --git a/app/templates/courses/course-details.jade b/app/templates/courses/course-details.jade
index 6118f447b..7b0ac0cf7 100644
--- a/app/templates/courses/course-details.jade
+++ b/app/templates/courses/course-details.jade
@@ -106,6 +106,11 @@ block content
if previousLevelCompleted || view.teacherMode || !passedLastCompletedLevel || levelStatus
- var i18n = level.get('type') === 'course-ladder' ? 'play.compete' : 'home.play';
button.btn.btn-success.btn-play-level(data-level-slug=level.get('slug'), data-i18n=i18n, data-level-id=level.get('original'))
+ if view.showGameDevButtons
+ - var levelOriginal = level.get('original');
+ - var session = view.levelSessions.find(function(session) { return session.get('level').original === levelOriginal });
+ if session
+ a.btn.btn-warning(href="/play/game-dev-level/#{level.get('slug')}/#{session.id}") Play Game Dev
td
if view.userLevelStateMap[me.id]
div= view.userLevelStateMap[me.id][level.get('original')]
diff --git a/app/views/courses/CourseDetailsView.coffee b/app/views/courses/CourseDetailsView.coffee
index 8a659103d..0c03ab445 100644
--- a/app/views/courses/CourseDetailsView.coffee
+++ b/app/views/courses/CourseDetailsView.coffee
@@ -33,6 +33,7 @@ module.exports = class CourseDetailsView extends RootView
@classroom = new Classroom()
@levels = new Levels()
@courseInstances = new CourseInstances()
+ @showGameDevButtons = me.isAdmin() or window.amActually # TEMP while testing game dev level system
@supermodel.trackRequest @ownedClassrooms.fetchMine({data: {project: '_id'}})
@supermodel.trackRequest(@courses.fetch().then(=>
From c0a70cb2aba4b94e0b8a3358d223eef96002db35 Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Thu, 14 Jul 2016 08:58:43 -0700
Subject: [PATCH 12/58] Refactor level type checks for easy greppability
(level.isType)
---
app/lib/LevelLoader.coffee | 12 +++----
app/lib/LevelSetupManager.coffee | 2 +-
app/models/Level.coffee | 11 +++---
app/views/clans/ClanDetailsView.coffee | 2 +-
app/views/courses/CourseDetailsView.coffee | 8 ++---
.../component/ThangComponentConfigView.coffee | 2 +-
.../level/thangs/LevelThangEditView.coffee | 2 +-
.../editor/level/thangs/ThangsTabView.coffee | 20 +++++------
app/views/editor/verifier/VerifierView.coffee | 2 +-
app/views/ladder/SimulateTabView.coffee | 2 +-
app/views/play/CampaignView.coffee | 4 +--
app/views/play/SpectateView.coffee | 2 +-
app/views/play/level/ControlBarView.coffee | 18 +++++-----
app/views/play/level/LevelGoalsView.coffee | 2 +-
app/views/play/level/LevelHUDView.coffee | 2 +-
app/views/play/level/LevelLoadingView.coffee | 2 +-
app/views/play/level/PlayLevelView.coffee | 31 ++++++++--------
.../play/level/modal/HeroVictoryModal.coffee | 36 +++++++++----------
.../play/level/modal/VictoryModal.coffee | 2 +-
app/views/play/level/tome/DocFormatter.coffee | 2 +-
app/views/play/level/tome/Spell.coffee | 10 +++---
.../level/tome/SpellListTabEntryView.coffee | 1 -
.../level/tome/SpellPaletteEntryView.coffee | 2 +-
.../play/level/tome/SpellPaletteView.coffee | 4 +--
app/views/play/level/tome/SpellView.coffee | 8 ++---
app/views/play/level/tome/TomeView.coffee | 2 +-
.../play/level/tome/editor/zatanna.coffee | 14 ++++----
app/views/play/menu/GameMenuModal.coffee | 4 +--
app/views/play/menu/GuideView.coffee | 2 +-
app/views/play/menu/MultiplayerView.coffee | 10 +++---
server/models/AnalyticsLogEvent.coffee | 4 +--
server/models/LevelSession.coffee | 4 +--
32 files changed, 114 insertions(+), 115 deletions(-)
diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee
index 02b0b0753..d2a4c7bb1 100644
--- a/app/lib/LevelLoader.coffee
+++ b/app/lib/LevelLoader.coffee
@@ -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,9 @@ 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 (@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 +179,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 +188,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 +453,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
diff --git a/app/lib/LevelSetupManager.coffee b/app/lib/LevelSetupManager.coffee
index 78d0c7205..f9d00a823 100644
--- a/app/lib/LevelSetupManager.coffee
+++ b/app/lib/LevelSetupManager.coffee
@@ -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') or window.serverConfig.picoCTF
@onInventoryModalPlayClicked()
return
@heroesModal = new PlayHeroesModal({supermodel: @supermodel, session: @session, confirmButtonI18N: 'play.next', level: @level, hadEverChosenHero: @options.hadEverChosenHero})
diff --git a/app/models/Level.coffee b/app/models/Level.coffee
index acad5b8a6..268b1700f 100644
--- a/app/models/Level.coffee
+++ b/app/models/Level.coffee
@@ -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')
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"
diff --git a/app/views/clans/ClanDetailsView.coffee b/app/views/clans/ClanDetailsView.coffee
index 8e8d7bc12..c9bcdb1e3 100644
--- a/app/views/clans/ClanDetailsView.coffee
+++ b/app/views/clans/ClanDetailsView.coffee
@@ -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?()
diff --git a/app/views/courses/CourseDetailsView.coffee b/app/views/courses/CourseDetailsView.coffee
index 0c03ab445..84b128df6 100644
--- a/app/views/courses/CourseDetailsView.coffee
+++ b/app/views/courses/CourseDetailsView.coffee
@@ -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
diff --git a/app/views/editor/component/ThangComponentConfigView.coffee b/app/views/editor/component/ThangComponentConfigView.coffee
index dc5498b41..08b20ebf9 100644
--- a/app/views/editor/component/ThangComponentConfigView.coffee
+++ b/app/views/editor/component/ThangComponentConfigView.coffee
@@ -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')
schema.required = []
treemaOptions =
supermodel: @supermodel
diff --git a/app/views/editor/level/thangs/LevelThangEditView.coffee b/app/views/editor/level/thangs/LevelThangEditView.coffee
index 84429d644..5dc7bc3f0 100644
--- a/app/views/editor/level/thangs/LevelThangEditView.coffee
+++ b/app/views/editor/level/thangs/LevelThangEditView.coffee
@@ -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') then options.thangType = thangType
@thangComponentEditView = new ThangComponentsEditView options
@listenTo @thangComponentEditView, 'components-changed', @onComponentsChanged
diff --git a/app/views/editor/level/thangs/ThangsTabView.coffee b/app/views/editor/level/thangs/ThangsTabView.coffee
index 1e794a40d..c2a631f47 100644
--- a/app/views/editor/level/thangs/ThangsTabView.coffee
+++ b/app/views/editor/level/thangs/ThangsTabView.coffee
@@ -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') 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')
components = [] # Load them all from default ThangType Components
else
components = _.cloneDeep thangType.get('components') ? []
diff --git a/app/views/editor/verifier/VerifierView.coffee b/app/views/editor/verifier/VerifierView.coffee
index 32c8e22e2..41a9500a6 100644
--- a/app/views/editor/verifier/VerifierView.coffee
+++ b/app/views/editor/verifier/VerifierView.coffee
@@ -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'] # Would use isType, but it's not a Level model
campaignInfo.levels.push level.slug
filterCodeLanguages: ->
diff --git a/app/views/ladder/SimulateTabView.coffee b/app/views/ladder/SimulateTabView.coffee
index 2be7ed186..4fac4239d 100644
--- a/app/views/ladder/SimulateTabView.coffee
+++ b/app/views/ladder/SimulateTabView.coffee
@@ -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: ->
diff --git a/app/views/play/CampaignView.coffee b/app/views/play/CampaignView.coffee
index 931a5244c..641f19e1e 100644
--- a/app/views/play/CampaignView.coffee
+++ b/app/views/play/CampaignView.coffee
@@ -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
diff --git a/app/views/play/SpectateView.coffee b/app/views/play/SpectateView.coffee
index 272b69d5b..b9122bd59 100644
--- a/app/views/play/SpectateView.coffee
+++ b/app/views/play/SpectateView.coffee
@@ -181,7 +181,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
diff --git a/app/views/play/level/ControlBarView.coffee b/app/views/play/level/ControlBarView.coffee
index 8254e9b18..6e07caa8a 100644
--- a/app/views/play/level/ControlBarView.coffee
+++ b/app/views/play/level/ControlBarView.coffee
@@ -45,7 +45,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') and @level.get('campaignIndex')?
@levelNumber = @level.get('campaignIndex') + 1
if @courseInstanceID
@courseInstance = new CourseInstance(_id: @courseInstanceID)
@@ -64,7 +64,7 @@ module.exports = class ControlBarView extends CocoView
@supermodel.trackRequest(@campaign.fetch())
)
super options
- if @level.get('type') in ['hero-ladder', 'course-ladder'] and me.isAdmin()
+ if @level.isType('hero-ladder', 'course-ladder') and me.isAdmin()
@isMultiplayerLevel = true
@multiplayerStatusManager = new MultiplayerStatusManager @levelID, @onMultiplayerStateChanged
if @level.get 'replayable'
@@ -95,7 +95,7 @@ module.exports = class ControlBarView extends CocoView
super c
c.worldName = @worldName
c.multiplayerEnabled = @session.get('multiplayer')
- c.ladderGame = @level.get('type') in ['ladder', 'hero-ladder', 'course-ladder']
+ c.ladderGame = @level.isType('ladder', 'hero-ladder', 'course-ladder')
if c.isMultiplayerLevel = @isMultiplayerLevel
c.multiplayerStatus = @multiplayerStatusManager?.status
if @level.get 'replayable'
@@ -110,23 +110,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 +136,7 @@ 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
@homeLink = '/'
@homeViewClass = 'views/HomeView'
@@ -153,7 +153,7 @@ 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()
diff --git a/app/views/play/level/LevelGoalsView.coffee b/app/views/play/level/LevelGoalsView.coffee
index 1c37e1245..d4749ae88 100644
--- a/app/views/play/level/LevelGoalsView.coffee
+++ b/app/views/play/level/LevelGoalsView.coffee
@@ -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'
diff --git a/app/views/play/level/LevelHUDView.coffee b/app/views/play/level/LevelHUDView.coffee
index 661da3aaf..b2cad7500 100644
--- a/app/views/play/level/LevelHUDView.coffee
+++ b/app/views/play/level/LevelHUDView.coffee
@@ -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'
diff --git a/app/views/play/level/LevelLoadingView.coffee b/app/views/play/level/LevelLoadingView.coffee
index eb9024d14..c0b3b82f3 100644
--- a/app/views/play/level/LevelLoadingView.coffee
+++ b/app/views/play/level/LevelLoadingView.coffee
@@ -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 $('' + name + '')
++goalCount
diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee
index 45340f0d7..d2148fb2d 100644
--- a/app/views/play/level/PlayLevelView.coffee
+++ b/app/views/play/level/PlayLevelView.coffee
@@ -205,7 +205,7 @@ module.exports = class PlayLevelView extends RootView
@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']
+ @$el.addClass 'hero' if @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev')
@$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
@@ -271,7 +271,7 @@ module.exports = class PlayLevelView extends RootView
@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')
#_.delay (=> Backbone.Mediator.publish('level:set-debug', debug: true)), 5000 if @isIPadApp() # if me.displayName() is 'Nick'
@@ -310,12 +310,12 @@ 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']
+ @onRealTimeMultiplayerLevelLoaded e.session if e.level.isType('hero-ladder', 'course-ladder')
onLoaded: ->
_.defer => @onLevelLoaderLoaded()
@@ -325,7 +325,7 @@ 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()
@@ -360,7 +360,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'
@@ -386,7 +386,7 @@ module.exports = class PlayLevelView extends RootView
@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()
@@ -440,7 +440,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 +462,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')
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 +541,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')
@showVictory() if e.showModal
return if @victorySeen
@victorySeen = true
@@ -560,7 +559,7 @@ 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 = if @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev') then HeroVictoryModal else VictoryModal
ModalClass = CourseVictoryModal if @isCourseMode() or me.isSessionless()
ModalClass = PicoCTFVictoryModal if window.serverConfig.picoCTF
victoryModal = new ModalClass(options)
diff --git a/app/views/play/level/modal/HeroVictoryModal.coffee b/app/views/play/level/modal/HeroVictoryModal.coffee
index 1f7a46672..66eed4f9b 100644
--- a/app/views/play/level/modal/HeroVictoryModal.coffee
+++ b/app/views/play/level/modal/HeroVictoryModal.coffee
@@ -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')
achievements = new CocoCollection([], {
url: "/db/achievement?related=#{@session.get('level').original}"
model: Achievement
@@ -63,14 +63,14 @@ module.exports = class HeroVictoryModal extends ModalView
else
@readyToContinue = true
@playSound 'victory'
- if @level.get('type', true) is 'course'
+ if @level.isType('course')
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
@@ -155,7 +155,7 @@ module.exports = class HeroVictoryModal extends ModalView
c = super()
c.levelName = utils.i18n @level.attributes, 'name'
# TODO: support 'game-dev'
- if @level.get('type', true) not in ['hero', 'game-dev']
+ if @level.isType('hero', 'game-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') # TODO: support game-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') # TODO: support game-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') # TODO: support game-dev
@sequentialAnimatedPanels = _.map(@animatedPanels.find('.reward-panel'), (panel) -> {
number: $(panel).data('number')
previousNumber: $(panel).data('previous-number')
@@ -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') and nextLevel = @level.get('nextLevel') and not returnToCourse
# 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
diff --git a/app/views/play/level/modal/VictoryModal.coffee b/app/views/play/level/modal/VictoryModal.coffee
index fcc8bdb9b..360ff0937 100644
--- a/app/views/play/level/modal/VictoryModal.coffee
+++ b/app/views/play/level/modal/VictoryModal.coffee
@@ -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
diff --git a/app/views/play/level/tome/DocFormatter.coffee b/app/views/play/level/tome/DocFormatter.coffee
index 93cdd1d6f..ec4e14cbd 100644
--- a/app/views/play/level/tome/DocFormatter.coffee
+++ b/app/views/play/level/tome/DocFormatter.coffee
@@ -139,7 +139,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: ->
diff --git a/app/views/play/level/tome/Spell.coffee b/app/views/play/level/tome/Spell.coffee
index 71b68fb3d..f34f99fe1 100644
--- a/app/views/play/level/tome/Spell.coffee
+++ b/app/views/play/level/tome/Spell.coffee
@@ -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
@@ -49,7 +47,7 @@ module.exports = class Spell
@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
hintsState: options.hintsState
@@ -87,7 +85,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:'
@@ -169,9 +167,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
diff --git a/app/views/play/level/tome/SpellListTabEntryView.coffee b/app/views/play/level/tome/SpellListTabEntryView.coffee
index b15798bbb..aabe6683a 100644
--- a/app/views/play/level/tome/SpellListTabEntryView.coffee
+++ b/app/views/play/level/tome/SpellListTabEntryView.coffee
@@ -37,7 +37,6 @@ module.exports = class SpellListTabEntryView extends SpellListEntryView
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: ->
diff --git a/app/views/play/level/tome/SpellPaletteEntryView.coffee b/app/views/play/level/tome/SpellPaletteEntryView.coffee
index cbada6610..7e7b883c9 100644
--- a/app/views/play/level/tome/SpellPaletteEntryView.coffee
+++ b/app/views/play/level/tome/SpellPaletteEntryView.coffee
@@ -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')
diff --git a/app/views/play/level/tome/SpellPaletteView.coffee b/app/views/play/level/tome/SpellPaletteView.coffee
index 87f7b21f4..42954a37d 100644
--- a/app/views/play/level/tome/SpellPaletteView.coffee
+++ b/app/views/play/level/tome/SpellPaletteView.coffee
@@ -157,7 +157,7 @@ module.exports = class SpellPaletteView extends CocoView
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') or not @options.programmable
@organizePalette propStorage, allDocs, excludedDocs
else
@organizePaletteHero propStorage, allDocs, excludedDocs
@@ -199,7 +199,7 @@ module.exports = class SpellPaletteView extends CocoView
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') then 'play_level.tome_your_skills' else 'play_level.tome_available_spells'
defaultGroup = $.i18n.t i18nKey
@entryGroups = {}
@entryGroups[defaultGroup] = @entries
diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee
index c3e2b7e14..834757ac2 100644
--- a/app/views/play/level/tome/SpellView.coffee
+++ b/app/views/play/level/tome/SpellView.coffee
@@ -502,7 +502,7 @@ 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.
@@ -554,7 +554,7 @@ module.exports = class SpellView extends CocoView
@createToolbarView()
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') # 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()
@@ -811,7 +811,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 +859,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
diff --git a/app/views/play/level/tome/TomeView.coffee b/app/views/play/level/tome/TomeView.coffee
index 442b8cf4b..14c3057c0 100644
--- a/app/views/play/level/tome/TomeView.coffee
+++ b/app/views/play/level/tome/TomeView.coffee
@@ -60,7 +60,7 @@ module.exports = class TomeView extends CocoView
@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']
+ unless @options.level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev')
@spellList = @insertSubView new SpellListView spells: @spells, supermodel: @supermodel, level: @options.level
@castButton = @insertSubView new CastButtonView spells: @spells, level: @options.level, session: @options.session, god: @options.god
@teamSpellMap = @generateTeamSpellMap(@spells)
diff --git a/app/views/play/level/tome/editor/zatanna.coffee b/app/views/play/level/tome/editor/zatanna.coffee
index 81d2ef5d6..57907b17e 100644
--- a/app/views/play/level/tome/editor/zatanna.coffee
+++ b/app/views/play/level/tome/editor/zatanna.coffee
@@ -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:'
diff --git a/app/views/play/menu/GameMenuModal.coffee b/app/views/play/menu/GameMenuModal.coffee
index a62a47cc2..efe19ec2f 100644
--- a/app/views/play/menu/GameMenuModal.coffee
+++ b/app/views/play/menu/GameMenuModal.coffee
@@ -35,7 +35,7 @@ module.exports = class GameMenuModal extends ModalView
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'])
+ submenus = _.without submenus, 'multiplayer' unless me.isAdmin() or (@level?.isType('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
@@ -47,7 +47,7 @@ module.exports = class GameMenuModal extends ModalView
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
diff --git a/app/views/play/menu/GuideView.coffee b/app/views/play/menu/GuideView.coffee
index d456ad687..fcd794117 100644
--- a/app/views/play/menu/GuideView.coffee
+++ b/app/views/play/menu/GuideView.coffee
@@ -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')
@helpVideos = if @isCourseLevel then [] else options.level.get('helpVideos') ? []
@trackedHelpVideoStart = @trackedHelpVideoFinish = false
# A/B Testing video tutorial styles
diff --git a/app/views/play/menu/MultiplayerView.coffee b/app/views/play/menu/MultiplayerView.coffee
index 13b28f149..4925744ce 100644
--- a/app/views/play/menu/MultiplayerView.coffee
+++ b/app/views/play/menu/MultiplayerView.coffee
@@ -27,7 +27,7 @@ module.exports = class MultiplayerView extends CocoView
@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()
+ @watchRealTimeSessions() if @level?.isType('hero-ladder', 'course-ladder') and me.isAdmin()
destroy: ->
@realTimeSessions?.off 'add', @onRealTimeSessionAdded
@@ -42,12 +42,12 @@ module.exports = class MultiplayerView extends CocoView
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']
+ if @level?.isType('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()
+ if @level?.isType('hero-ladder', 'course-ladder') and me.isAdmin()
c.levelID = @session.get('levelID')
c.realTimeSessions = @realTimeSessions
c.currentRealTimeSession = @currentRealTimeSession if @currentRealTimeSession
@@ -76,7 +76,7 @@ module.exports = class MultiplayerView extends CocoView
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'
+ leagueType = if @level?.isType('course-ladder') then 'course' else 'clan'
viewArgs.push leagueType
viewArgs.push leagueID
ladderURL += "/#{leagueType}/#{leagueID}"
@@ -86,7 +86,7 @@ module.exports = class MultiplayerView extends CocoView
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)
+ la.toggle if @level?.isType('ladder', 'hero-ladder', 'course-ladder') then false else Boolean(multiplayer)
true
onHidden: ->
diff --git a/server/models/AnalyticsLogEvent.coffee b/server/models/AnalyticsLogEvent.coffee
index e824ad776..e00234607 100644
--- a/server/models/AnalyticsLogEvent.coffee
+++ b/server/models/AnalyticsLogEvent.coffee
@@ -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)
diff --git a/server/models/LevelSession.coffee b/server/models/LevelSession.coffee
index bc3076fdc..f61c9f4f8 100644
--- a/server/models/LevelSession.coffee
+++ b/server/models/LevelSession.coffee
@@ -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
From 349ab24da7bc9ab3628019bb332b0f9573ee143c Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Thu, 14 Jul 2016 09:38:45 -0700
Subject: [PATCH 13/58] First pass at adding 'web-dev' level type
---
app/core/ParticleMan.coffee | 36 +++++++++++++++++++
app/lib/LevelSetupManager.coffee | 2 +-
app/models/Level.coffee | 2 +-
.../component/ThangComponentConfigView.coffee | 2 +-
.../level/thangs/LevelThangEditView.coffee | 2 +-
.../editor/level/thangs/ThangsTabView.coffee | 4 +--
app/views/editor/verifier/VerifierView.coffee | 2 +-
app/views/play/level/ControlBarView.coffee | 3 +-
app/views/play/level/PlayLevelView.coffee | 8 ++---
.../play/level/modal/HeroVictoryModal.coffee | 18 +++++-----
.../play/level/tome/SpellPaletteView.coffee | 4 +--
app/views/play/level/tome/TomeView.coffee | 2 +-
app/views/play/menu/GuideView.coffee | 2 +-
13 files changed, 62 insertions(+), 25 deletions(-)
diff --git a/app/core/ParticleMan.coffee b/app/core/ParticleMan.coffee
index bcaebd72b..41be7829c 100644
--- a/app/core/ParticleMan.coffee
+++ b/app/core/ParticleMan.coffee
@@ -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
diff --git a/app/lib/LevelSetupManager.coffee b/app/lib/LevelSetupManager.coffee
index f9d00a823..840967c12 100644
--- a/app/lib/LevelSetupManager.coffee
+++ b/app/lib/LevelSetupManager.coffee
@@ -74,7 +74,7 @@ module.exports = class LevelSetupManager extends CocoClass
@session.set 'heroConfig', {"thangType":raider,"inventory":{}}
@onInventoryModalPlayClicked()
return
- if @level.isType('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})
diff --git a/app/models/Level.coffee b/app/models/Level.coffee
index 268b1700f..998e40c2c 100644
--- a/app/models/Level.coffee
+++ b/app/models/Level.coffee
@@ -59,7 +59,7 @@ module.exports = class Level extends CocoModel
denormalize: (supermodel, session, otherSession) ->
o = $.extend true, {}, @attributes
- if o.thangs and @isType('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
diff --git a/app/views/editor/component/ThangComponentConfigView.coffee b/app/views/editor/component/ThangComponentConfigView.coffee
index 08b20ebf9..2182e2a8c 100644
--- a/app/views/editor/component/ThangComponentConfigView.coffee
+++ b/app/views/editor/component/ThangComponentConfigView.coffee
@@ -46,7 +46,7 @@ module.exports = class ThangComponentConfigView extends CocoView
schema.default ?= {}
_.merge schema.default, @additionalDefaults if @additionalDefaults
- if @level?.isType('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
diff --git a/app/views/editor/level/thangs/LevelThangEditView.coffee b/app/views/editor/level/thangs/LevelThangEditView.coffee
index 5dc7bc3f0..46da5067d 100644
--- a/app/views/editor/level/thangs/LevelThangEditView.coffee
+++ b/app/views/editor/level/thangs/LevelThangEditView.coffee
@@ -41,7 +41,7 @@ module.exports = class LevelThangEditView extends CocoView
level: @level
world: @world
- if @level.isType('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
diff --git a/app/views/editor/level/thangs/ThangsTabView.coffee b/app/views/editor/level/thangs/ThangsTabView.coffee
index c2a631f47..a62c28460 100644
--- a/app/views/editor/level/thangs/ThangsTabView.coffee
+++ b/app/views/editor/level/thangs/ThangsTabView.coffee
@@ -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.isType('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.isType('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') ? []
diff --git a/app/views/editor/verifier/VerifierView.coffee b/app/views/editor/verifier/VerifierView.coffee
index 41a9500a6..423ea104d 100644
--- a/app/views/editor/verifier/VerifierView.coffee
+++ b/app/views/editor/verifier/VerifierView.coffee
@@ -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'] # Would use isType, but it's not a Level model
+ 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: ->
diff --git a/app/views/play/level/ControlBarView.coffee b/app/views/play/level/ControlBarView.coffee
index 6e07caa8a..000256999 100644
--- a/app/views/play/level/ControlBarView.coffee
+++ b/app/views/play/level/ControlBarView.coffee
@@ -45,7 +45,7 @@ module.exports = class ControlBarView extends CocoView
@observing = options.session.get('creator') isnt me.id
@levelNumber = ''
- if @level.isType('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)
@@ -137,6 +137,7 @@ module.exports = class ControlBarView extends CocoView
@homeLink += "/#{@courseInstanceID}"
@homeViewArgs.push @courseInstanceID
#else if @level.isType('game-dev') # TODO
+ #else if @level.isType('web-dev') # TODO
else
@homeLink = '/'
@homeViewClass = 'views/HomeView'
diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee
index d2148fb2d..d0a4179f6 100644
--- a/app/views/play/level/PlayLevelView.coffee
+++ b/app/views/play/level/PlayLevelView.coffee
@@ -205,7 +205,7 @@ module.exports = class PlayLevelView extends RootView
@session = @levelLoader.session
@world = @levelLoader.world
@level = @levelLoader.level
- @$el.addClass 'hero' if @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev')
+ @$el.addClass 'hero' if @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-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
@@ -468,7 +468,7 @@ module.exports = class PlayLevelView extends RootView
return false if $.browser?.msie or $.browser?.msedge
return false if $.browser.linux
return false if me.level() < 8
- if @level.isType('course', 'game-dev')
+ if @level.isType('course', 'game-dev', 'web-dev')
return false
else if @level.isType('hero') and gamesSimulated
return false if stillBuggy
@@ -541,7 +541,7 @@ module.exports = class PlayLevelView extends RootView
onDonePressed: -> @showVictory()
onShowVictory: (e) ->
- $('#level-done-button').show() unless @level.isType('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
@@ -559,7 +559,7 @@ 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.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev') then HeroVictoryModal else VictoryModal
+ 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()
ModalClass = PicoCTFVictoryModal if window.serverConfig.picoCTF
victoryModal = new ModalClass(options)
diff --git a/app/views/play/level/modal/HeroVictoryModal.coffee b/app/views/play/level/modal/HeroVictoryModal.coffee
index 66eed4f9b..182a2ce4c 100644
--- a/app/views/play/level/modal/HeroVictoryModal.coffee
+++ b/app/views/play/level/modal/HeroVictoryModal.coffee
@@ -49,7 +49,7 @@ module.exports = class HeroVictoryModal extends ModalView
@session = options.session
@level = options.level
@thangTypes = {}
- if @level.isType('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,7 +63,7 @@ module.exports = class HeroVictoryModal extends ModalView
else
@readyToContinue = true
@playSound 'victory'
- if @level.isType('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
@@ -73,7 +73,7 @@ module.exports = class HeroVictoryModal extends ModalView
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.isType('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 [])
@@ -223,7 +223,7 @@ module.exports = class HeroVictoryModal extends ModalView
afterRender: ->
super()
- @$el.toggleClass 'with-achievements', @level.isType('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()
@@ -233,7 +233,7 @@ module.exports = class HeroVictoryModal extends ModalView
@insertSubView @ladderSubmissionView, @$el.find('.ladder-submission-view')
initializeAnimations: ->
- return @endSequentialAnimations() unless @level.isType('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.isType('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')
@@ -414,7 +414,7 @@ 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.isType('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')}"
diff --git a/app/views/play/level/tome/SpellPaletteView.coffee b/app/views/play/level/tome/SpellPaletteView.coffee
index 42954a37d..930f30faf 100644
--- a/app/views/play/level/tome/SpellPaletteView.coffee
+++ b/app/views/play/level/tome/SpellPaletteView.coffee
@@ -157,7 +157,7 @@ module.exports = class SpellPaletteView extends CocoView
else
propStorage =
'this': ['apiProperties', 'apiMethods']
- if not @options.level.isType('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
@@ -199,7 +199,7 @@ module.exports = class SpellPaletteView extends CocoView
if tabbify and _.find @entries, ((entry) -> entry.doc.owner isnt 'this')
@entryGroups = _.groupBy @entries, groupForEntry
else
- i18nKey = if @options.level.isType('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
diff --git a/app/views/play/level/tome/TomeView.coffee b/app/views/play/level/tome/TomeView.coffee
index 14c3057c0..0f5a23e17 100644
--- a/app/views/play/level/tome/TomeView.coffee
+++ b/app/views/play/level/tome/TomeView.coffee
@@ -60,7 +60,7 @@ module.exports = class TomeView extends CocoView
@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.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev')
+ unless @options.level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev')
@spellList = @insertSubView new SpellListView spells: @spells, supermodel: @supermodel, level: @options.level
@castButton = @insertSubView new CastButtonView spells: @spells, level: @options.level, session: @options.session, god: @options.god
@teamSpellMap = @generateTeamSpellMap(@spells)
diff --git a/app/views/play/menu/GuideView.coffee b/app/views/play/menu/GuideView.coffee
index fcd794117..e64c2c2a0 100644
--- a/app/views/play/menu/GuideView.coffee
+++ b/app/views/play/menu/GuideView.coffee
@@ -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.isType('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
From c5c831c2117b5702020413d2fb43eef8d8ee3cae Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Thu, 14 Jul 2016 10:26:09 -0700
Subject: [PATCH 14/58] Remove real-time multiplayer prototype code
---
app/collections/RealTimeCollection.coffee | 6 -
app/core/Router.coffee | 2 -
app/core/initialize.coffee | 3 +-
app/lib/LevelBus.coffee | 12 +-
app/lib/surface/Surface.coffee | 9 +-
app/lib/surface/WaitingScreen.coffee | 65 ---
app/locale/en.coffee | 12 +-
app/models/LevelSession.coffee | 2 -
app/models/RealTimeModel.coffee | 6 -
app/schemas/models/level_session.coffee | 2 -
app/schemas/subscriptions/multiplayer.coffee | 21 -
app/schemas/subscriptions/play.coffee | 2 -
app/styles/play/level/control_bar.sass | 36 --
app/styles/play/menu/multiplayer-view.sass | 8 -
app/templates/play/level/control_bar.jade | 23 +-
app/templates/play/menu/multiplayer-view.jade | 90 ----
app/views/ladder/MainLadderView.coffee | 48 +--
app/views/play/SpectateView.coffee | 3 -
app/views/play/level/ControlBarView.coffee | 84 ----
app/views/play/level/LevelChatView.coffee | 1 +
app/views/play/level/LevelFlagsView.coffee | 38 +-
app/views/play/level/LevelPlaybackView.coffee | 6 -
app/views/play/level/PlayLevelView.coffee | 390 +-----------------
.../play/level/tome/CastButtonView.coffee | 13 +-
.../play/level/tome/ProblemAlertView.coffee | 1 -
app/views/play/level/tome/SpellView.coffee | 44 +-
app/views/play/menu/GameMenuModal.coffee | 6 +-
app/views/play/menu/MultiplayerView.coffee | 229 ----------
server/models/LevelSession.coffee | 2 +-
29 files changed, 36 insertions(+), 1128 deletions(-)
delete mode 100644 app/collections/RealTimeCollection.coffee
delete mode 100644 app/lib/surface/WaitingScreen.coffee
delete mode 100644 app/models/RealTimeModel.coffee
delete mode 100644 app/schemas/subscriptions/multiplayer.coffee
delete mode 100644 app/styles/play/menu/multiplayer-view.sass
delete mode 100644 app/templates/play/menu/multiplayer-view.jade
delete mode 100644 app/views/play/menu/MultiplayerView.coffee
diff --git a/app/collections/RealTimeCollection.coffee b/app/collections/RealTimeCollection.coffee
deleted file mode 100644
index a5af239ed..000000000
--- a/app/collections/RealTimeCollection.coffee
+++ /dev/null
@@ -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()
diff --git a/app/core/Router.coffee b/app/core/Router.coffee
index 4890b9bb5..de3530738 100644
--- a/app/core/Router.coffee
+++ b/app/core/Router.coffee
@@ -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')
diff --git a/app/core/initialize.coffee b/app/core/initialize.coffee
index 7acbc0cdc..b81841a6b 100644
--- a/app/core/initialize.coffee
+++ b/app/core/initialize.coffee
@@ -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()
diff --git a/app/lib/LevelBus.coffee b/app/lib/LevelBus.coffee
index 741420cca..aab23bf16 100644
--- a/app/lib/LevelBus.coffee
+++ b/app/lib/LevelBus.coffee
@@ -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 = {}
diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee
index 01980a2d8..4f63e1ce3 100644
--- a/app/lib/surface/Surface.coffee
+++ b/app/lib/surface/Surface.coffee
@@ -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()
diff --git a/app/lib/surface/WaitingScreen.coffee b/app/lib/surface/WaitingScreen.coffee
deleted file mode 100644
index 232ddb5f8..000000000
--- a/app/lib/surface/WaitingScreen.coffee
+++ /dev/null
@@ -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: -> ''
-
- 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()
diff --git a/app/locale/en.coffee b/app/locale/en.coffee
index 143010bb6..04bd218f0 100644
--- a/app/locale/en.coffee
+++ b/app/locale/en.coffee
@@ -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"
@@ -1890,16 +1890,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."
diff --git a/app/models/LevelSession.coffee b/app/models/LevelSession.coffee
index 1d4334535..23b68851c 100644
--- a/app/models/LevelSession.coffee
+++ b/app/models/LevelSession.coffee
@@ -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) ->
diff --git a/app/models/RealTimeModel.coffee b/app/models/RealTimeModel.coffee
deleted file mode 100644
index 217c72f2e..000000000
--- a/app/models/RealTimeModel.coffee
+++ /dev/null
@@ -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()
diff --git a/app/schemas/models/level_session.coffee b/app/schemas/models/level_session.coffee
index 95be39816..2c7bbe1fe 100644
--- a/app/schemas/models/level_session.coffee
+++ b/app/schemas/models/level_session.coffee
@@ -37,8 +37,6 @@ _.extend LevelSessionSchema.properties,
type: 'string'
levelID:
type: 'string'
- multiplayer:
- type: 'boolean'
creator: c.objectId
links:
[
diff --git a/app/schemas/subscriptions/multiplayer.coffee b/app/schemas/subscriptions/multiplayer.coffee
deleted file mode 100644
index fd88f449c..000000000
--- a/app/schemas/subscriptions/multiplayer.coffee
+++ /dev/null
@@ -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'}
diff --git a/app/schemas/subscriptions/play.coffee b/app/schemas/subscriptions/play.coffee
index 1c4adc9b4..e03736613 100644
--- a/app/schemas/subscriptions/play.coffee
+++ b/app/schemas/subscriptions/play.coffee
@@ -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 {}
diff --git a/app/styles/play/level/control_bar.sass b/app/styles/play/level/control_bar.sass
index a31844c8e..48f835534 100644
--- a/app/styles/play/level/control_bar.sass
+++ b/app/styles/play/level/control_bar.sass
@@ -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)
diff --git a/app/styles/play/menu/multiplayer-view.sass b/app/styles/play/menu/multiplayer-view.sass
deleted file mode 100644
index c1bfbee23..000000000
--- a/app/styles/play/menu/multiplayer-view.sass
+++ /dev/null
@@ -1,8 +0,0 @@
-#multiplayer-view
- textarea
- width: 100%
- box-sizing: border-box
- padding: 5px
- text-align: center
- height: 30px
- font-size: 11px
diff --git a/app/templates/play/level/control_bar.jade b/app/templates/play/level/control_bar.jade
index 639a389f8..2f6c56148 100644
--- a/app/templates/play/level/control_bar.jade
+++ b/app/templates/play/level/control_bar.jade
@@ -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
diff --git a/app/templates/play/menu/multiplayer-view.jade b/app/templates/play/menu/multiplayer-view.jade
deleted file mode 100644
index c45a1259c..000000000
--- a/app/templates/play/menu/multiplayer-view.jade
+++ /dev/null
@@ -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
diff --git a/app/views/ladder/MainLadderView.coffee b/app/views/ladder/MainLadderView.coffee
index b4108ee42..171f8b3ee 100644
--- a/app/views/ladder/MainLadderView.coffee
+++ b/app/views/ladder/MainLadderView.coffee
@@ -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}
]
diff --git a/app/views/play/SpectateView.coffee b/app/views/play/SpectateView.coffee
index b9122bd59..f5a28fac8 100644
--- a/app/views/play/SpectateView.coffee
+++ b/app/views/play/SpectateView.coffee
@@ -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 = =>
diff --git a/app/views/play/level/ControlBarView.coffee b/app/views/play/level/ControlBarView.coffee
index 000256999..895c6f0fc 100644
--- a/app/views/play/level/ControlBarView.coffee
+++ b/app/views/play/level/ControlBarView.coffee
@@ -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) ->
@@ -64,9 +59,6 @@ module.exports = class ControlBarView extends CocoView
@supermodel.trackRequest(@campaign.fetch())
)
super options
- if @level.isType('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.isType('ladder', 'hero-ladder', 'course-ladder')
- if c.isMultiplayerLevel = @isMultiplayerLevel
- c.multiplayerStatus = @multiplayerStatusManager?.status
if @level.get 'replayable'
c.levelDifficulty = @session.get('state')?.difficulty ? 0
if @observing
@@ -161,9 +138,6 @@ module.exports = class ControlBarView extends CocoView
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
@@ -184,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()
diff --git a/app/views/play/level/LevelChatView.coffee b/app/views/play/level/LevelChatView.coffee
index 06c218294..122b15b7a 100644
--- a/app/views/play/level/LevelChatView.coffee
+++ b/app/views/play/level/LevelChatView.coffee
@@ -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)
diff --git a/app/views/play/level/LevelFlagsView.coffee b/app/views/play/level/LevelFlagsView.coffee
index 9bdb04c4a..83b2a71ce 100644
--- a/app/views/play/level/LevelFlagsView.coffee
+++ b/app/views/play/level/LevelFlagsView.coffee
@@ -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) =>
diff --git a/app/views/play/level/LevelPlaybackView.coffee b/app/views/play/level/LevelPlaybackView.coffee
index 6547a683b..811d431a1 100644
--- a/app/views/play/level/LevelPlaybackView.coffee
+++ b/app/views/play/level/LevelPlaybackView.coffee
@@ -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()
diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee
index d0a4179f6..26ce79d54 100644
--- a/app/views/play/level/PlayLevelView.coffee
+++ b/app/views/play/level/PlayLevelView.coffee
@@ -73,13 +73,8 @@ module.exports = class PlayLevelView extends RootView
'level:loading-view-unveiling': 'onLoadingViewUnveiling'
'level:loading-view-unveiled': 'onLoadingViewUnveiled'
'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'
@@ -192,8 +187,6 @@ module.exports = class PlayLevelView extends RootView
@initGoalManager()
@insertSubviews()
@initVolume()
- @listenTo(@session, 'change:multiplayer', @onMultiplayerChanged)
-
@register()
@controlBar.setBus(@bus)
@initScriptManager()
@@ -239,9 +232,6 @@ 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: ->
@god.setLevel @level.serialize {@supermodel, @session, @otherSession, headless: false, sessionless: false}
@@ -289,9 +279,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 ######################################################
@@ -315,8 +303,6 @@ module.exports = class PlayLevelView extends RootView
@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.isType('hero-ladder', 'course-ladder')
-
onLoaded: ->
_.defer => @onLevelLoaderLoaded()
@@ -393,9 +379,6 @@ module.exports = class PlayLevelView extends RootView
@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'
@@ -584,13 +567,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
@@ -629,10 +605,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()
@@ -645,14 +617,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'
@@ -677,7 +647,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()
@@ -693,358 +662,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 ######################################################
diff --git a/app/views/play/level/tome/CastButtonView.coffee b/app/views/play/level/tome/CastButtonView.coffee
index deade19b4..c40dec5a6 100644
--- a/app/views/play/level/tome/CastButtonView.coffee
+++ b/app/views/play/level/tome/CastButtonView.coffee
@@ -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'
@@ -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}
@@ -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
diff --git a/app/views/play/level/tome/ProblemAlertView.coffee b/app/views/play/level/tome/ProblemAlertView.coffee
index c6508eb17..c9ffc1363 100644
--- a/app/views/play/level/tome/ProblemAlertView.coffee
+++ b/app/views/play/level/tome/ProblemAlertView.coffee
@@ -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'
diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee
index 834757ac2..db465c974 100644
--- a/app/views/play/level/tome/SpellView.coffee
+++ b/app/views/play/level/tome/SpellView.coffee
@@ -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
@@ -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
@@ -502,8 +496,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 +506,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 +519,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
@@ -573,7 +559,7 @@ module.exports = class SpellView extends CocoView
@saveSpade()
getSource: ->
- @ace.getValue() # could also do @firepad.getText()
+ @ace.getValue()
setThang: (thang) ->
@focus()
diff --git a/app/views/play/menu/GameMenuModal.coffee b/app/views/play/menu/GameMenuModal.coffee
index efe19ec2f..b0489865c 100644
--- a/app/views/play/menu/GameMenuModal.coffee
+++ b/app/views/play/menu/GameMenuModal.coffee
@@ -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?.isType('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,7 +41,6 @@ module.exports = class GameMenuModal extends ModalView
'options': 'cog'
'guide': 'list'
'save-load': 'floppy-disk'
- 'multiplayer': 'globe'
context
showsChooseHero: ->
@@ -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
diff --git a/app/views/play/menu/MultiplayerView.coffee b/app/views/play/menu/MultiplayerView.coffee
deleted file mode 100644
index 4925744ce..000000000
--- a/app/views/play/menu/MultiplayerView.coffee
+++ /dev/null
@@ -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?.isType('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?.isType('ladder', 'hero-ladder', 'course-ladder')
- c.ladderGame = true
- c.readyToRank = @session?.readyToRank()
-
- # Real-time multiplayer stuff
- if @level?.isType('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?.isType('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?.isType('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()
diff --git a/server/models/LevelSession.coffee b/server/models/LevelSession.coffee
index f61c9f4f8..b68263841 100644
--- a/server/models/LevelSession.coffee
+++ b/server/models/LevelSession.coffee
@@ -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',
From 16b10612b6e05eece49be1713e728d8c99b0829b Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Thu, 14 Jul 2016 12:34:22 -0700
Subject: [PATCH 15/58] Stub WebSurface showing for web-dev levels
---
.../javascripts/workers/aether_worker.js | 1 +
.../javascripts/workers/worker_world.js | 2 +-
app/core/treema-ext.coffee | 16 +++-----
app/core/utils.coffee | 14 +++----
app/lib/LevelLoader.coffee | 3 ++
app/schemas/models/campaign.schema.coffee | 2 +-
app/schemas/models/level.coffee | 2 +-
app/styles/play/play-level-view.sass | 20 ++++++++++
app/templates/play/play-level-view.jade | 2 +
app/views/play/level/LevelLoadingView.coffee | 2 +-
app/views/play/level/PlayLevelView.coffee | 39 +++++++++++++------
app/views/play/level/tome/DocFormatter.coffee | 1 +
app/views/play/level/tome/Spell.coffee | 3 ++
.../level/tome/SpellListTabEntryView.coffee | 1 +
app/views/play/level/tome/SpellView.coffee | 11 +++---
app/views/play/level/tome/TomeView.coffee | 23 ++++++++++-
16 files changed, 102 insertions(+), 40 deletions(-)
diff --git a/app/assets/javascripts/workers/aether_worker.js b/app/assets/javascripts/workers/aether_worker.js
index 80c72bf54..285e6600d 100644
--- a/app/assets/javascripts/workers/aether_worker.js
+++ b/app/assets/javascripts/workers/aether_worker.js
@@ -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;
};
diff --git a/app/assets/javascripts/workers/worker_world.js b/app/assets/javascripts/workers/worker_world.js
index e40889e04..5857ba810 100644
--- a/app/assets/javascripts/workers/worker_world.js
+++ b/app/assets/javascripts/workers/worker_world.js
@@ -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;
};
diff --git a/app/core/treema-ext.coffee b/app/core/treema-ext.coffee
index f64f6f3cb..685ccb1fd 100644
--- a/app/core/treema-ext.coffee
+++ b/app/core/treema-ext.coffee
@@ -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: ->
diff --git a/app/core/utils.coffee b/app/core/utils.coffee
index 07e90f4f5..ca921ba82 100644
--- a/app/core/utils.coffee
+++ b/app/core/utils.coffee
@@ -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  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()
diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee
index d2a4c7bb1..3f300f09e 100644
--- a/app/lib/LevelLoader.coffee
+++ b/app/lib/LevelLoader.coffee
@@ -74,6 +74,8 @@ module.exports = class LevelLoader extends CocoClass
onLevelLoaded: ->
if not @sessionless and @level.isType('hero', 'hero-ladder', 'hero-coop', 'course')
@sessionDependenciesRegistered = {}
+ if @level.isType('web-dev')
+ @headless = true
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
@@ -481,6 +483,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
diff --git a/app/schemas/models/campaign.schema.coffee b/app/schemas/models/campaign.schema.coffee
index d5e0a1c8a..ff78c8263 100644
--- a/app/schemas/models/campaign.schema.coffee
+++ b/app/schemas/models/campaign.schema.coffee
@@ -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' }
diff --git a/app/schemas/models/level.coffee b/app/schemas/models/level.coffee
index b7b826e76..15d928dfa 100644
--- a/app/schemas/models/level.coffee
+++ b/app/schemas/models/level.coffee
@@ -313,7 +313,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'}
diff --git a/app/styles/play/play-level-view.sass b/app/styles/play/play-level-view.sass
index 8b3c027ae..afbc33772 100644
--- a/app/styles/play/play-level-view.sass
+++ b/app/styles/play/play-level-view.sass
@@ -272,6 +272,26 @@ $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
+ width: 100%
+ height: 100%
+
html.fullscreen-editor
#level-view
#fullscreen-editor-background-screen
diff --git a/app/templates/play/play-level-view.jade b/app/templates/play/play-level-view.jade
index a225ea79b..aa1b46d64 100644
--- a/app/templates/play/play-level-view.jade
+++ b/app/templates/play/play-level-view.jade
@@ -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
#ascii-surface
#canvas-left-gradient.gradient
#canvas-top-gradient.gradient
diff --git a/app/views/play/level/LevelLoadingView.coffee b/app/views/play/level/LevelLoadingView.coffee
index c0b3b82f3..6e47711a0 100644
--- a/app/views/play/level/LevelLoadingView.coffee
+++ b/app/views/play/level/LevelLoadingView.coffee
@@ -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
diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee
index 26ce79d54..0ce0ba314 100644
--- a/app/views/play/level/PlayLevelView.coffee
+++ b/app/views/play/level/PlayLevelView.coffee
@@ -132,7 +132,7 @@ module.exports = class PlayLevelView extends RootView
load: ->
@loadStartTime = new Date()
- @god = new God({@gameUIState})
+ @god = new God({@gameUIState}) # TODO: don't make one of these in web-dev mode
levelLoaderOptions = supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @opponentSessionID, team: @getQueryVariable('team'), observing: @observing, courseID: @courseID
if me.isSessionless()
levelLoaderOptions.fakeSessionConfig = {}
@@ -196,9 +196,12 @@ module.exports = class PlayLevelView extends RootView
grabLevelLoaderData: ->
@session = @levelLoader.session
- @world = @levelLoader.world
@level = @levelLoader.level
- @$el.addClass 'hero' if @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev') # TODO: figure out what this does and comment it
+ 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
@@ -234,6 +237,7 @@ module.exports = class PlayLevelView extends RootView
@session.set('code', myCode)
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
@@ -252,12 +256,12 @@ module.exports = class PlayLevelView extends RootView
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
@@ -272,6 +276,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()
@@ -318,7 +323,10 @@ module.exports = class PlayLevelView extends RootView
@saveRecentMatch() if @otherSession
@levelLoader.destroy()
@levelLoader = null
- @initSurface()
+ if @level.isType('web-dev')
+ @initWebSurface()
+ else
+ @initSurface()
saveRecentMatch: ->
allRecentlyPlayedMatches = storage.load('recently-played-matches') ? {}
@@ -355,12 +363,13 @@ 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()
+ @webSurface?.showLevel()
Backbone.Mediator.publish 'level:set-time', time: 0
if (@isEditorPreview or @observing) and not @getQueryVariable('intro')
@loadingView.startUnveiling()
@@ -406,7 +415,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()
@@ -662,3 +671,11 @@ module.exports = class PlayLevelView extends RootView
@setupManager?.destroy()
@setupManager = new LevelSetupManager({supermodel: @supermodel, level: @level, levelID: @levelID, parent: @, session: @session, hadEverChosenHero: true})
@setupManager.open()
+
+
+ # web-dev levels
+ initWebSurface: ->
+ @webSurface = showLevel: =>
+ # TODO: make a real WebSurface class
+ @$('#web-surface').css('background-color', 'red')
+ Backbone.Mediator.publish 'level:started', {}
diff --git a/app/views/play/level/tome/DocFormatter.coffee b/app/views/play/level/tome/DocFormatter.coffee
index ec4e14cbd..54dfbb6ae 100644
--- a/app/views/play/level/tome/DocFormatter.coffee
+++ b/app/views/play/level/tome/DocFormatter.coffee
@@ -42,6 +42,7 @@ 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
diff --git a/app/views/play/level/tome/Spell.coffee b/app/views/play/level/tome/Spell.coffee
index f34f99fe1..0f910c587 100644
--- a/app/views/play/level/tome/Spell.coffee
+++ b/app/views/play/level/tome/Spell.coffee
@@ -65,6 +65,7 @@ module.exports = class Spell
@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
@@ -126,6 +127,8 @@ module.exports = class Spell
else
source = @getSource()
[pure, problems] = [null, null]
+ if @language is 'html'
+ [pure, problems] = [source, []] # TODO: problems? Actually do something when transpiling
for thangID, spellThang of @thangs
unless pure
pure = spellThang.aether.transpile source
diff --git a/app/views/play/level/tome/SpellListTabEntryView.coffee b/app/views/play/level/tome/SpellListTabEntryView.coffee
index aabe6683a..bdee757ba 100644
--- a/app/views/play/level/tome/SpellListTabEntryView.coffee
+++ b/app/views/play/level/tome/SpellListTabEntryView.coffee
@@ -55,6 +55,7 @@ module.exports = class SpellListTabEntryView extends SpellListEntryView
@buildDocs() unless @docsBuilt
buildAvatar: ->
+ return unless @thang.world
avatar = new ThangAvatarView thang: @thang, includeName: false, supermodel: @supermodel
if @avatar
@avatar.$el.replaceWith avatar.$el
diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee
index db465c974..ffaf9ebcd 100644
--- a/app/views/play/level/tome/SpellView.coffee
+++ b/app/views/play/level/tome/SpellView.coffee
@@ -226,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
@@ -470,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
@@ -864,7 +865,9 @@ module.exports = class SpellView extends CocoView
beginningOfLine = not currentLine.substr(0, cursorPosition.column).trim().length # uncommenting code, for example
incompleteThis = /^(s|se|sel|self|t|th|thi|this)$/.test currentLine.trim()
#console.log "finished=#{valid and (endOfLine or beginningOfLine) and not incompleteThis}", valid, endOfLine, beginningOfLine, incompleteThis, cursorPosition, currentLine.length, aether, new Date() - 0, currentLine
- if valid and (endOfLine or beginningOfLine) and not incompleteThis
+ if @options.level.isType('web-dev') and valid
+ console.log 'Update it!'
+ else if valid and (endOfLine or beginningOfLine) and not incompleteThis
@preload()
singleLineCommentRegex: ->
@@ -976,8 +979,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) ->
@@ -1246,7 +1247,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
diff --git a/app/views/play/level/tome/TomeView.coffee b/app/views/play/level/tome/TomeView.coffee
index 0f5a23e17..efae8f136 100644
--- a/app/views/play/level/tome/TomeView.coffee
+++ b/app/views/play/level/tome/TomeView.coffee
@@ -59,6 +59,9 @@ module.exports = class TomeView extends CocoView
super()
@worker = @createWorker()
programmableThangs = _.filter @options.thangs, (t) -> t.isProgrammable and t.programmableMethods
+ if @options.level.isType('web-dev')
+ if @fakeProgrammableThang = @createFakeProgrammableThang()
+ programmableThangs = [@fakeProgrammableThang]
@createSpells programmableThangs, programmableThangs[0]?.world # Do before spellList, thangList, and castButton
unless @options.level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev')
@spellList = @insertSubView new SpellListView spells: @spells, supermodel: @supermodel, level: @options.level
@@ -140,7 +143,7 @@ module.exports = class TomeView extends CocoView
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 +164,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
@@ -194,7 +198,7 @@ module.exports = class TomeView extends CocoView
@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.
+ 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.
thang = e.thang
spellName = e.spellName
@spellList?.$el.hide()
@@ -204,6 +208,9 @@ module.exports = class TomeView extends CocoView
@clearSpellView()
@updateSpellPalette thang, spell if spell
return
+ @setSpellView spell, thang
+
+ setSpellView: (spell, thang) ->
unless spell.view is @spellView
@clearSpellView()
@spellView = spell.view
@@ -246,6 +253,9 @@ module.exports = class TomeView extends CocoView
@cast()
onSelectPrimarySprite: (e) ->
+ if @options.level.isType('web-dev')
+ @setSpellView @spells['hero-placeholder/plan'], @fakeProgrammableThang
+ return
# This is only fired by PlayLevelView for hero levels currently
# TODO: Don't hard code these hero names
if @options.session.get('team') is 'ogres'
@@ -253,6 +263,15 @@ module.exports = class TomeView extends CocoView
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
+ thang =
+ id: 'Hero Placeholder'
+ isProgrammable: true
+ thang = _.merge thang, programmableConfig
+ thang
+
destroy: ->
spell.destroy() for spellKey, spell of @spells
@worker?.terminate()
From be50657530d009d90464d02c9b8b90806a1f741c Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Thu, 14 Jul 2016 12:47:00 -0700
Subject: [PATCH 16/58] Remove Firebase for now.
---
app/lib/Bus.coffee | 1 +
bower.json | 5 -----
2 files changed, 1 insertion(+), 5 deletions(-)
diff --git a/app/lib/Bus.coffee b/app/lib/Bus.coffee
index ffe508754..2180dae4b 100644
--- a/app/lib/Bus.coffee
+++ b/app/lib/Bus.coffee
@@ -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)
diff --git a/bower.json b/bower.json
index 333ffe8ec..4e27947f6 100644
--- a/bower.json
+++ b/bower.json
@@ -34,7 +34,6 @@
"moment": "~2.5.0",
"aether": "~0.5.6",
"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"
},
From 5b16da099a9d9810b1e4b7a8468664bd07fd55c8 Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Thu, 14 Jul 2016 12:47:25 -0700
Subject: [PATCH 17/58] Hack LevelEditor to load web-dev levels
---
app/lib/LevelLoader.coffee | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee
index 3f300f09e..297fd4b89 100644
--- a/app/lib/LevelLoader.coffee
+++ b/app/lib/LevelLoader.coffee
@@ -76,6 +76,13 @@ module.exports = class LevelLoader extends CocoClass
@sessionDependenciesRegistered = {}
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
From 0cb92582f4f8d06b372ec0847f33fa9163c84640 Mon Sep 17 00:00:00 2001
From: Scott Erickson
Date: Thu, 14 Jul 2016 15:13:02 -0700
Subject: [PATCH 18/58] Add destroy method
---
app/views/play/level/PlayGameDevLevelView.coffee | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/app/views/play/level/PlayGameDevLevelView.coffee b/app/views/play/level/PlayGameDevLevelView.coffee
index ab7e2fb9e..93815b580 100644
--- a/app/views/play/level/PlayGameDevLevelView.coffee
+++ b/app/views/play/level/PlayGameDevLevelView.coffee
@@ -80,3 +80,12 @@ module.exports = class PlayGameDevLevelView extends RootView
Backbone.Mediator.publish('playback:real-time-playback-started', {})
Backbone.Mediator.publish('level:set-playing', {playing: true})
@state.set('playing', true)
+
+ destroy: ->
+ @levelLoader?.destroy()
+ @surface?.destroy()
+ @god?.destroy()
+ @goalManager?.destroy()
+ @scriptManager?.destroy()
+ delete window.world # not sure where this is set, but this is one way to clean it up
+ super()
From c0bc10ffb67f700eff350a5b9e72d0f9b66dc3d9 Mon Sep 17 00:00:00 2001
From: Scott Erickson
Date: Thu, 14 Jul 2016 15:53:54 -0700
Subject: [PATCH 19/58] Add projects tab stub to TeacherClassView
---
app/templates/courses/teacher-class-view.jade | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/app/templates/courses/teacher-class-view.jade b/app/templates/courses/teacher-class-view.jade
index e6e025574..4df8d4acb 100644
--- a/app/templates/courses/teacher-class-view.jade
+++ b/app/templates/courses/teacher-class-view.jade
@@ -122,6 +122,10 @@ block content
li(class=(activeTab === "#enrollment-status-tab" ? 'active' : ''))
a.course-progress-tab-btn(href='#enrollment-status-tab')
.small-details.text-center(data-i18n='teacher.enrollment_status')
+ .tab-spacer
+ li(class=(activeTab === "#student-projects-tab" ? 'active' : ''))
+ a.course-progress-tab-btn(href='#student-projects-tab')
+ .small-details.text-center(data-i18n='') Projects
.tab-filler
.tab-content
@@ -129,8 +133,10 @@ block content
+studentsTab
else if activeTab === '#course-progress-tab'
+courseProgressTab
- else
+ else if activeTab === '#enrollment-status-tab'
+enrollmentStatusTab
+ else
+ +studentProjectsTab
else
.text-center.m-t-5.m-b-5
@@ -430,3 +436,6 @@ mixin enrollmentStatusTab
td.enroll-col
if status !== 'enrolled'
button.enroll-student-button.btn.btn-navy(data-i18n="teacher.enroll_student", data-user-id=student.id, data-event-action="Teachers Class Enrollment Enroll Student")
+
+mixin studentProjectsTab
+ p ...
From 9a79cae09d9a756a234c3b99a5b66e075de35f3f Mon Sep 17 00:00:00 2001
From: Scott Erickson
Date: Thu, 14 Jul 2016 16:49:48 -0700
Subject: [PATCH 20/58] Fix PlayGameDevLevelView to run in course mode
---
app/views/play/level/PlayGameDevLevelView.coffee | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/app/views/play/level/PlayGameDevLevelView.coffee b/app/views/play/level/PlayGameDevLevelView.coffee
index 93815b580..8c20469df 100644
--- a/app/views/play/level/PlayGameDevLevelView.coffee
+++ b/app/views/play/level/PlayGameDevLevelView.coffee
@@ -32,8 +32,9 @@ module.exports = class PlayGameDevLevelView extends RootView
@level = new Level()
@session = new LevelSession()
@gameUIState = new GameUIState()
+ @courseID = @getQueryVariable 'course'
@god = new God({ @gameUIState })
- @levelLoader = new LevelLoader({ @supermodel, @levelID, @sessionID, observing: true, team: TEAM })
+ @levelLoader = new LevelLoader({ @supermodel, @levelID, @sessionID, observing: true, team: TEAM, @courseID })
@listenTo @state, 'change', _.debounce(-> @renderSelectors('#info-col'))
@levelLoader.loadWorldNecessities()
From 9d0ad7af44362d0f04fa3df91df2b9dbabc20f61 Mon Sep 17 00:00:00 2001
From: Scott Erickson
Date: Thu, 14 Jul 2016 16:50:17 -0700
Subject: [PATCH 21/58] Start work on having course arenas use the
CourseVictoryModal
---
app/views/play/level/PlayLevelView.coffee | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee
index 45340f0d7..398a1709d 100644
--- a/app/views/play/level/PlayLevelView.coffee
+++ b/app/views/play/level/PlayLevelView.coffee
@@ -561,7 +561,11 @@ module.exports = class PlayLevelView extends RootView
@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()
+ ModalClass = CourseVictoryModal if @isCourseMode() or me.isSessionless()
+ if @level.get('type', true) is '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)
From ed320a8d9e1abee75a06fa9d2955de0bd2ed0fb5 Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Thu, 14 Jul 2016 18:07:36 -0700
Subject: [PATCH 22/58] WebSurfaceView now parsing player code through virtual
DOM into iframe
---
app/assets/javascripts/web-dev-listener.js | 45 ++++++++++++++++
app/assets/web-dev-iframe.html | 28 ++++++++++
app/schemas/subscriptions/tome.coffee | 3 ++
app/styles/play/level/web-surface-view.sass | 53 +++++++++++++++++++
app/styles/play/play-level-view.sass | 9 ++--
.../play/level/web-surface-view.jade | 1 +
app/templates/play/play-level-view.jade | 2 +-
app/views/editor/level/LevelEditView.coffee | 1 +
app/views/play/level/PlayLevelView.coffee | 13 ++---
app/views/play/level/WebSurfaceView.coffee | 43 +++++++++++++++
app/views/play/level/tome/Spell.coffee | 11 ++++
app/views/play/level/tome/SpellView.coffee | 13 +++--
bower.json | 5 +-
config.coffee | 2 +-
14 files changed, 207 insertions(+), 22 deletions(-)
create mode 100644 app/assets/javascripts/web-dev-listener.js
create mode 100644 app/assets/web-dev-iframe.html
create mode 100644 app/styles/play/level/web-surface-view.sass
create mode 100644 app/templates/play/level/web-surface-view.jade
create mode 100644 app/views/play/level/WebSurfaceView.coffee
diff --git a/app/assets/javascripts/web-dev-listener.js b/app/assets/javascripts/web-dev-listener.js
new file mode 100644
index 000000000..f4dd5aadc
--- /dev/null
+++ b/app/assets/javascripts/web-dev-listener.js
@@ -0,0 +1,45 @@
+// 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;
+
+function receiveMessage(event) {
+ var origin = event.origin || event.originalEvent.origin; // For Chrome, the origin property is in the event.originalEvent object.
+ if (origin != 'https://codecombat.com' && origin != 'http://localhost:3000') {
+ console.log("Bad origin:", origin);
+ }
+ //console.log(event);
+ switch (event.data.type) {
+ case 'create':
+ case 'update':
+ if (virtualDOM)
+ update(event.data.dom);
+ else
+ create(event.data.dom);
+ break;
+ case 'log':
+ console.log(event.data.text);
+ break;
+ default:
+ console.log('Unknown message type:', event.data.type);
+ }
+
+ //event.source.postMessage("hi there yourself! the secret response is: rheeeeet!", event.origin);
+}
+
+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;
+}
diff --git a/app/assets/web-dev-iframe.html b/app/assets/web-dev-iframe.html
new file mode 100644
index 000000000..d3349ecc3
--- /dev/null
+++ b/app/assets/web-dev-iframe.html
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+ My CodeCombat Website
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Loading...
+
+
diff --git a/app/schemas/subscriptions/tome.coffee b/app/schemas/subscriptions/tome.coffee
index 983b746a3..eee398bd9 100644
--- a/app/schemas/subscriptions/tome.coffee
+++ b/app/schemas/subscriptions/tome.coffee
@@ -146,3 +146,6 @@ 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']},
+ html: {type: 'string'}
diff --git a/app/styles/play/level/web-surface-view.sass b/app/styles/play/level/web-surface-view.sass
new file mode 100644
index 000000000..ddfed6497
--- /dev/null
+++ b/app/styles/play/level/web-surface-view.sass
@@ -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
+//
diff --git a/app/styles/play/play-level-view.sass b/app/styles/play/play-level-view.sass
index afbc33772..a0a560ff9 100644
--- a/app/styles/play/play-level-view.sass
+++ b/app/styles/play/play-level-view.sass
@@ -288,9 +288,12 @@ $level-resize-transition-time: 0.5s
#canvas-wrapper canvas
display: none
- #web-surface
- width: 100%
- height: 100%
+ #web-surface-view
+ position: absolute
+ top: 0
+ right: 0
+ left: 0
+ bottom: 0
html.fullscreen-editor
#level-view
diff --git a/app/templates/play/level/web-surface-view.jade b/app/templates/play/level/web-surface-view.jade
new file mode 100644
index 000000000..f2991ad44
--- /dev/null
+++ b/app/templates/play/level/web-surface-view.jade
@@ -0,0 +1 @@
+iframe(src="/web-dev-iframe.html")
diff --git a/app/templates/play/play-level-view.jade b/app/templates/play/play-level-view.jade
index aa1b46d64..77508fcfb 100644
--- a/app/templates/play/play-level-view.jade
+++ b/app/templates/play/play-level-view.jade
@@ -25,7 +25,7 @@ if view.showAds()
canvas(width=924, height=589)#webgl-surface
canvas(width=924, height=589)#normal-surface
- #web-surface
+ #web-surface-view
#ascii-surface
#canvas-left-gradient.gradient
#canvas-top-gradient.gradient
diff --git a/app/views/editor/level/LevelEditView.coffee b/app/views/editor/level/LevelEditView.coffee
index bf86e6825..32c45bf9b 100644
--- a/app/views/editor/level/LevelEditView.coffee
+++ b/app/views/editor/level/LevelEditView.coffee
@@ -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'
diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee
index 0ce0ba314..9fd28e1f6 100644
--- a/app/views/play/level/PlayLevelView.coffee
+++ b/app/views/play/level/PlayLevelView.coffee
@@ -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
@@ -268,6 +269,7 @@ module.exports = class PlayLevelView extends RootView
@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 if @level.isType('web-dev')
#_.delay (=> Backbone.Mediator.publish('level:set-debug', debug: true)), 5000 if @isIPadApp() # if me.displayName() is 'Nick'
initVolume: ->
@@ -324,7 +326,7 @@ module.exports = class PlayLevelView extends RootView
@levelLoader.destroy()
@levelLoader = null
if @level.isType('web-dev')
- @initWebSurface()
+ Backbone.Mediator.publish 'level:started', {}
else
@initSurface()
@@ -369,7 +371,6 @@ module.exports = class PlayLevelView extends RootView
if window.currentModal and not window.currentModal.destroyed and window.currentModal.constructor isnt VictoryModal
return Backbone.Mediator.subscribeOnce 'modal:closed', @onLevelStarted, @
@surface?.showLevel()
- @webSurface?.showLevel()
Backbone.Mediator.publish 'level:set-time', time: 0
if (@isEditorPreview or @observing) and not @getQueryVariable('intro')
@loadingView.startUnveiling()
@@ -671,11 +672,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()
-
-
- # web-dev levels
- initWebSurface: ->
- @webSurface = showLevel: =>
- # TODO: make a real WebSurface class
- @$('#web-surface').css('background-color', 'red')
- Backbone.Mediator.publish 'level:started', {}
diff --git a/app/views/play/level/WebSurfaceView.coffee b/app/views/play/level/WebSurfaceView.coffee
new file mode 100644
index 000000000..3ed5f2b29
--- /dev/null
+++ b/app/views/play/level/WebSurfaceView.coffee
@@ -0,0 +1,43 @@
+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'
+ super(options)
+
+ afterRender: ->
+ super()
+ @iframe = @$('iframe')[0]
+ $(@iframe).on 'load', (e) =>
+ @iframe.contentWindow.postMessage {type: 'log', text: 'Player HTML iframe is ready.'}, "*"
+ @iframeLoaded = true
+ @onIframeLoaded?()
+ @onIframeLoaded = null
+
+ 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 @virtualDOM then 'update' else 'create'
+ @iframe.contentWindow.postMessage {type: messageType, dom: 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 ? []))
diff --git a/app/views/play/level/tome/Spell.coffee b/app/views/play/level/tome/Spell.coffee
index 0f910c587..676ce5dad 100644
--- a/app/views/play/level/tome/Spell.coffee
+++ b/app/views/play/level/tome/Spell.coffee
@@ -70,6 +70,14 @@ module.exports = class Spell
@originalSource = @languages[@language] ? @languages.javascript
@originalSource = @addPicoCTFProblem() if window.serverConfig.picoCTF
+ if @level.isType('web-dev')
+ playerCode = @originalSource.match(/\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 /[\s\S]*<\/playercode>/, '☃'
+ @originalSource = playerCode
+
# Translate comments chosen spoken language.
return unless @commentContext
context = $.extend true, {}, @commentContext
@@ -95,6 +103,9 @@ module.exports = class Spell
when 'coffeescript' then @originalSource
else @originalSource
+ constructHTML: (source) ->
+ @wrapperCode.replace '☃', source
+
addPicoCTFProblem: ->
return @originalSource unless problem = @level.picoCTFProblem
description = """
diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee
index ffaf9ebcd..52deaa8cc 100644
--- a/app/views/play/level/tome/SpellView.coffee
+++ b/app/views/play/level/tome/SpellView.coffee
@@ -539,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() if @options.level.isType('web-dev')
createDebugView: ->
- return if @options.level.isType('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()
@@ -712,6 +713,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.
@@ -724,6 +727,9 @@ module.exports = class SpellView extends CocoView
onCursorActivity: => # Used to refresh autocast delay; doesn't do anything at the moment.
+ updateHTML: =>
+ Backbone.Mediator.publish 'tome:html-updated', html: @spell.constructHTML @getSource()
+
# 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
@@ -865,9 +871,7 @@ module.exports = class SpellView extends CocoView
beginningOfLine = not currentLine.substr(0, cursorPosition.column).trim().length # uncommenting code, for example
incompleteThis = /^(s|se|sel|self|t|th|thi|this)$/.test currentLine.trim()
#console.log "finished=#{valid and (endOfLine or beginningOfLine) and not incompleteThis}", valid, endOfLine, beginningOfLine, incompleteThis, cursorPosition, currentLine.length, aether, new Date() - 0, currentLine
- if @options.level.isType('web-dev') and valid
- console.log 'Update it!'
- else if valid and (endOfLine or beginningOfLine) and not incompleteThis
+ if valid and (endOfLine or beginningOfLine) and not incompleteThis
@preload()
singleLineCommentRegex: ->
@@ -896,6 +900,7 @@ 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
diff --git a/bower.json b/bower.json
index 4e27947f6..962798f29 100644
--- a/bower.json
+++ b/bower.json
@@ -107,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"
]
}
},
diff --git a/config.coffee b/config.coffee
index 75e614786..739525c65 100644
--- a/config.coffee
+++ b/config.coffee
@@ -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')
From 33ba3f6033d124398c66449a20bd5766bdfc44b7 Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Thu, 14 Jul 2016 19:14:18 -0700
Subject: [PATCH 23/58] Enable docs for web-dev levels
---
.../play/level/tome/spell-palette-view.sass | 8 ++++++++
.../level/tome/spell_palette_entry_popover.jade | 2 +-
app/views/play/level/tome/DocFormatter.coffee | 3 +++
.../level/tome/SpellPaletteEntryView.coffee | 2 +-
.../play/level/tome/SpellPaletteView.coffee | 17 ++++++++++++-----
app/views/play/level/tome/TomeView.coffee | 6 ++++--
6 files changed, 29 insertions(+), 9 deletions(-)
diff --git a/app/styles/play/level/tome/spell-palette-view.sass b/app/styles/play/level/tome/spell-palette-view.sass
index 1ba004ff4..89fab6088 100644
--- a/app/styles/play/level/tome/spell-palette-view.sass
+++ b/app/styles/play/level/tome/spell-palette-view.sass
@@ -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
diff --git a/app/templates/play/level/tome/spell_palette_entry_popover.jade b/app/templates/play/level/tome/spell_palette_entry_popover.jade
index 5ae7d312c..879a30225 100644
--- a/app/templates/play/level/tome/spell_palette_entry_popover.jade
+++ b/app/templates/play/level/tome/spell_palette_entry_popover.jade
@@ -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
diff --git a/app/views/play/level/tome/DocFormatter.coffee b/app/views/play/level/tome/DocFormatter.coffee
index 54dfbb6ae..ef8500b77 100644
--- a/app/views/play/level/tome/DocFormatter.coffee
+++ b/app/views/play/level/tome/DocFormatter.coffee
@@ -49,6 +49,8 @@ module.exports = class DocFormatter
@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
@@ -186,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'
diff --git a/app/views/play/level/tome/SpellPaletteEntryView.coffee b/app/views/play/level/tome/SpellPaletteEntryView.coffee
index 7e7b883c9..c6332828c 100644
--- a/app/views/play/level/tome/SpellPaletteEntryView.coffee
+++ b/app/views/play/level/tome/SpellPaletteEntryView.coffee
@@ -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
diff --git a/app/views/play/level/tome/SpellPaletteView.coffee b/app/views/play/level/tome/SpellPaletteView.coffee
index 930f30faf..df31577d5 100644
--- a/app/views/play/level/tome/SpellPaletteView.coffee
+++ b/app/views/play/level/tome/SpellPaletteView.coffee
@@ -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,6 +156,7 @@ module.exports = class SpellPaletteView extends CocoView
JSON: 'programmableJSONProperties'
LoDash: 'programmableLoDashProperties'
Vector: 'programmableVectorProperties'
+ HTML: 'programmableHTMLProperties'
snippets: 'programmableSnippets'
else
propStorage =
@@ -192,7 +196,7 @@ 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
@@ -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] =
diff --git a/app/views/play/level/tome/TomeView.coffee b/app/views/play/level/tome/TomeView.coffee
index efae8f136..62dcf0e49 100644
--- a/app/views/play/level/tome/TomeView.coffee
+++ b/app/views/play/level/tome/TomeView.coffee
@@ -225,7 +225,7 @@ module.exports = class TomeView extends CocoView
@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
@@ -266,10 +266,12 @@ module.exports = class TomeView extends CocoView
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
+ thang = _.merge thang, programmableConfig, usesHTMLConfig
thang
destroy: ->
From 220db3106cbbe3ba47292a6db2e8de13dd628488 Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Thu, 14 Jul 2016 19:48:27 -0700
Subject: [PATCH 24/58] Run button now recreates web-dev DOM; no submit button
---
app/assets/javascripts/web-dev-listener.js | 2 ++
app/schemas/subscriptions/tome.coffee | 5 +++--
app/views/play/level/WebSurfaceView.coffee | 8 +++++++-
app/views/play/level/tome/CastButtonView.coffee | 2 +-
app/views/play/level/tome/SpellView.coffee | 7 ++++---
5 files changed, 17 insertions(+), 7 deletions(-)
diff --git a/app/assets/javascripts/web-dev-listener.js b/app/assets/javascripts/web-dev-listener.js
index f4dd5aadc..41ce0788b 100644
--- a/app/assets/javascripts/web-dev-listener.js
+++ b/app/assets/javascripts/web-dev-listener.js
@@ -13,6 +13,8 @@ function receiveMessage(event) {
//console.log(event);
switch (event.data.type) {
case 'create':
+ create(event.data.dom);
+ break;
case 'update':
if (virtualDOM)
update(event.data.dom);
diff --git a/app/schemas/subscriptions/tome.coffee b/app/schemas/subscriptions/tome.coffee
index eee398bd9..65b8b275b 100644
--- a/app/schemas/subscriptions/tome.coffee
+++ b/app/schemas/subscriptions/tome.coffee
@@ -147,5 +147,6 @@ module.exports =
'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']},
- html: {type: 'string'}
+ '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)'}
diff --git a/app/views/play/level/WebSurfaceView.coffee b/app/views/play/level/WebSurfaceView.coffee
index 3ed5f2b29..4b0bdc9ab 100644
--- a/app/views/play/level/WebSurfaceView.coffee
+++ b/app/views/play/level/WebSurfaceView.coffee
@@ -22,6 +22,8 @@ module.exports = class WebSurfaceView extends CocoView
@iframeLoaded = true
@onIframeLoaded?()
@onIframeLoaded = null
+
+ # TODO: make clicking Run actually trigger a 'create' update here (for resetting scripts)
onHTMLUpdated: (e) ->
unless @iframeLoaded
@@ -31,8 +33,12 @@ module.exports = class WebSurfaceView extends CocoView
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 @virtualDOM then 'update' else 'create'
+ messageType = if e.create or not @virtualDOM then 'create' else 'update'
@iframe.contentWindow.postMessage {type: messageType, dom: virtualDOM}, '*'
+ @virtualDOM = virtualDOM
+
+ checkGoals: (dom) ->
+ # TODO: uhh, figure these out
dekuify: (elem) ->
return elem.data if elem.type is 'text'
diff --git a/app/views/play/level/tome/CastButtonView.coffee b/app/views/play/level/tome/CastButtonView.coffee
index c40dec5a6..6a1e2b28e 100644
--- a/app/views/play/level/tome/CastButtonView.coffee
+++ b/app/views/play/level/tome/CastButtonView.coffee
@@ -40,7 +40,7 @@ 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'
@$el.find('.done-button').show()
diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee
index 52deaa8cc..91c3e8c35 100644
--- a/app/views/play/level/tome/SpellView.coffee
+++ b/app/views/play/level/tome/SpellView.coffee
@@ -539,7 +539,7 @@ 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() if @options.level.isType('web-dev')
+ @updateHTML create: true if @options.level.isType('web-dev')
createDebugView: ->
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.
@@ -663,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
@@ -727,8 +728,8 @@ module.exports = class SpellView extends CocoView
onCursorActivity: => # Used to refresh autocast delay; doesn't do anything at the moment.
- updateHTML: =>
- Backbone.Mediator.publish 'tome:html-updated', html: @spell.constructHTML @getSource()
+ 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
From e3670165e71fb0c6e9c2b3bd38f465af404a459f Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Thu, 14 Jul 2016 22:43:25 -0700
Subject: [PATCH 25/58] Remove code for multiple spells; rename
SpellListTabEntryView to SpellTopBarView; remove hero avatar from
SpellTopBarView
---
app/lib/God.coffee | 4 +-
app/locale/en.coffee | 2 -
app/schemas/subscriptions/tome.coffee | 6 -
...ist_entry.sass => spell-top-bar-view.sass} | 56 +--------
app/styles/play/level/tome/spell_list.sass | 20 ---
.../level/tome/spell_list_entry_thangs.sass | 30 -----
...tab_entry.jade => spell-top-bar-view.jade} | 6 -
app/templates/play/level/tome/spell_list.jade | 1 -
.../play/level/tome/spell_list_entry.jade | 7 --
.../level/tome/spell_list_entry_thangs.jade | 3 -
app/templates/play/level/tome/tome.jade | 6 +-
app/views/play/level/ThangAvatarView.coffee | 9 +-
app/views/play/level/tome/Spell.coffee | 50 +++-----
.../play/level/tome/SpellDebugView.coffee | 5 -
.../tome/SpellListEntryThangsView.coffee | 33 -----
.../play/level/tome/SpellListEntryView.coffee | 115 ------------------
.../play/level/tome/SpellListView.coffee | 96 ---------------
...ntryView.coffee => SpellTopBarView.coffee} | 78 ++----------
app/views/play/level/tome/SpellView.coffee | 36 +++---
app/views/play/level/tome/TomeView.coffee | 108 +++++-----------
20 files changed, 78 insertions(+), 593 deletions(-)
rename app/styles/play/level/tome/{spell_list_entry.sass => spell-top-bar-view.sass} (66%)
delete mode 100644 app/styles/play/level/tome/spell_list.sass
delete mode 100644 app/styles/play/level/tome/spell_list_entry_thangs.sass
rename app/templates/play/level/tome/{spell_list_tab_entry.jade => spell-top-bar-view.jade} (78%)
delete mode 100644 app/templates/play/level/tome/spell_list.jade
delete mode 100644 app/templates/play/level/tome/spell_list_entry.jade
delete mode 100644 app/templates/play/level/tome/spell_list_entry_thangs.jade
delete mode 100644 app/views/play/level/tome/SpellListEntryThangsView.coffee
delete mode 100644 app/views/play/level/tome/SpellListEntryView.coffee
delete mode 100644 app/views/play/level/tome/SpellListView.coffee
rename app/views/play/level/tome/{SpellListTabEntryView.coffee => SpellTopBarView.coffee} (55%)
diff --git a/app/lib/God.coffee b/app/lib/God.coffee
index 272213028..60c15c5b6 100644
--- a/app/lib/God.coffee
+++ b/app/lib/God.coffee
@@ -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
diff --git a/app/locale/en.coffee b/app/locale/en.coffee
index 04bd218f0..416dfead9 100644
--- a/app/locale/en.coffee
+++ b/app/locale/en.coffee
@@ -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"
diff --git a/app/schemas/subscriptions/tome.coffee b/app/schemas/subscriptions/tome.coffee
index 65b8b275b..06008d209 100644
--- a/app/schemas/subscriptions/tome.coffee
+++ b/app/schemas/subscriptions/tome.coffee
@@ -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.'}
diff --git a/app/styles/play/level/tome/spell_list_entry.sass b/app/styles/play/level/tome/spell-top-bar-view.sass
similarity index 66%
rename from app/styles/play/level/tome/spell_list_entry.sass
rename to app/styles/play/level/tome/spell-top-bar-view.sass
index 3f7e3e4df..39207901d 100644
--- a/app/styles/play/level/tome/spell_list_entry.sass
+++ b/app/styles/play/level/tome/spell-top-bar-view.sass
@@ -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
diff --git a/app/styles/play/level/tome/spell_list.sass b/app/styles/play/level/tome/spell_list.sass
deleted file mode 100644
index b66cd5f82..000000000
--- a/app/styles/play/level/tome/spell_list.sass
+++ /dev/null
@@ -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
diff --git a/app/styles/play/level/tome/spell_list_entry_thangs.sass b/app/styles/play/level/tome/spell_list_entry_thangs.sass
deleted file mode 100644
index abe03b44a..000000000
--- a/app/styles/play/level/tome/spell_list_entry_thangs.sass
+++ /dev/null
@@ -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
diff --git a/app/templates/play/level/tome/spell_list_tab_entry.jade b/app/templates/play/level/tome/spell-top-bar-view.jade
similarity index 78%
rename from app/templates/play/level/tome/spell_list_tab_entry.jade
rename to app/templates/play/level/tome/spell-top-bar-view.jade
index 5f5b02af9..d9341fb29 100644
--- a/app/templates/play/level/tome/spell_list_tab_entry.jade
+++ b/app/templates/play/level/tome/spell-top-bar-view.jade
@@ -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
diff --git a/app/templates/play/level/tome/spell_list.jade b/app/templates/play/level/tome/spell_list.jade
deleted file mode 100644
index 84b28511a..000000000
--- a/app/templates/play/level/tome/spell_list.jade
+++ /dev/null
@@ -1 +0,0 @@
-h5(data-i18n="play_level.tome_select_method") Select a Method
diff --git a/app/templates/play/level/tome/spell_list_entry.jade b/app/templates/play/level/tome/spell_list_entry.jade
deleted file mode 100644
index 561591078..000000000
--- a/app/templates/play/level/tome/spell_list_entry.jade
+++ /dev/null
@@ -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})
-
-
diff --git a/app/templates/play/level/tome/spell_list_entry_thangs.jade b/app/templates/play/level/tome/spell_list_entry_thangs.jade
deleted file mode 100644
index bcc7804f1..000000000
--- a/app/templates/play/level/tome/spell_list_entry_thangs.jade
+++ /dev/null
@@ -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(", ")})
diff --git a/app/templates/play/level/tome/tome.jade b/app/templates/play/level/tome/tome.jade
index c29f9e4f0..526d8bc4d 100644
--- a/app/templates/play/level/tome/tome.jade
+++ b/app/templates/play/level/tome/tome.jade
@@ -1,11 +1,7 @@
-#spell-list-tab-entry-view
-
-#spell-list-view
+#spell-top-bar-view
#cast-button-view
#spell-view
#spell-palette-view
-
-
diff --git a/app/views/play/level/ThangAvatarView.coffee b/app/views/play/level/ThangAvatarView.coffee
index f16fcea96..1d6c05fb0 100644
--- a/app/views/play/level/ThangAvatarView.coffee
+++ b/app/views/play/level/ThangAvatarView.coffee
@@ -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
diff --git a/app/views/play/level/tome/Spell.coffee b/app/views/play/level/tome/Spell.coffee
index 676ce5dad..d1e119f28 100644
--- a/app/views/play/level/tome/Spell.coffee
+++ b/app/views/play/level/tome/Spell.coffee
@@ -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
@@ -45,23 +45,22 @@ 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, 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) ->
@@ -115,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
@@ -137,30 +136,16 @@ module.exports = class Spell
@source = source
else
source = @getSource()
- [pure, problems] = [null, null]
- if @language is 'html'
- [pure, problems] = [source, []] # TODO: problems? Actually do something when transpiling
- 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 =
@@ -202,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'
diff --git a/app/views/play/level/tome/SpellDebugView.coffee b/app/views/play/level/tome/SpellDebugView.coffee
index ddc787731..d02314259 100644
--- a/app/views/play/level/tome/SpellDebugView.coffee
+++ b/app/views/play/level/tome/SpellDebugView.coffee
@@ -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
diff --git a/app/views/play/level/tome/SpellListEntryThangsView.coffee b/app/views/play/level/tome/SpellListEntryThangsView.coffee
deleted file mode 100644
index 599cabe51..000000000
--- a/app/views/play/level/tome/SpellListEntryThangsView.coffee
+++ /dev/null
@@ -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()
diff --git a/app/views/play/level/tome/SpellListEntryView.coffee b/app/views/play/level/tome/SpellListEntryView.coffee
deleted file mode 100644
index 92c5462d0..000000000
--- a/app/views/play/level/tome/SpellListEntryView.coffee
+++ /dev/null
@@ -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()
diff --git a/app/views/play/level/tome/SpellListView.coffee b/app/views/play/level/tome/SpellListView.coffee
deleted file mode 100644
index 59a966d75..000000000
--- a/app/views/play/level/tome/SpellListView.coffee
+++ /dev/null
@@ -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()
diff --git a/app/views/play/level/tome/SpellListTabEntryView.coffee b/app/views/play/level/tome/SpellTopBarView.coffee
similarity index 55%
rename from app/views/play/level/tome/SpellListTabEntryView.coffee
rename to app/views/play/level/tome/SpellTopBarView.coffee
index bdee757ba..fe1814df2 100644
--- a/app/views/play/level/tome/SpellListTabEntryView.coffee
+++ b/app/views/play/level/tome/SpellTopBarView.coffee
@@ -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,7 +32,6 @@ 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
@@ -44,52 +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: ->
- return unless @thang.world
- 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()
diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee
index 91c3e8c35..bda7f943c 100644
--- a/app/views/play/level/tome/SpellView.coffee
+++ b/app/views/play/level/tome/SpellView.coffee
@@ -569,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
@@ -612,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
@@ -903,15 +903,12 @@ module.exports = class SpellView extends CocoView
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
@@ -928,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
@@ -952,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
@@ -1149,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
diff --git a/app/views/play/level/tome/TomeView.coffee b/app/views/play/level/tome/TomeView.coffee
index 62dcf0e49..c35766665 100644
--- a/app/views/play/level/tome/TomeView.coffee
+++ b/app/views/play/level/tome/TomeView.coffee
@@ -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,7 +41,6 @@ module.exports = class TomeView extends CocoView
'tome:select-primary-sprite': 'onSelectPrimarySprite'
events:
- 'click #spell-view': 'onSpellViewClick'
'click': 'onClick'
afterRender: ->
@@ -62,9 +50,7 @@ module.exports = class TomeView extends CocoView
if @options.level.isType('web-dev')
if @fakeProgrammableThang = @createFakeProgrammableThang()
programmableThangs = [@fakeProgrammableThang]
- @createSpells programmableThangs, programmableThangs[0]?.world # Do before spellList, thangList, and castButton
- unless @options.level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev')
- @spellList = @insertSubView new SpellListView spells: @spells, supermodel: @supermodel, level: @options.level
+ @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
@@ -75,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()
@@ -117,30 +101,27 @@ 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 = @fakeProgrammableThang ? world.getThangByID thangID
@@ -176,53 +157,24 @@ 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('').detach()
- @spellView = null
- @spellTabView?.$el.after('').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', 'web-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
- @setSpellView spell, thang
+ 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 or thang.programmableHTMLProperties)
@@ -256,7 +208,7 @@ module.exports = class TomeView extends CocoView
if @options.level.isType('web-dev')
@setSpellView @spells['hero-placeholder/plan'], @fakeProgrammableThang
return
- # This is only fired by PlayLevelView for hero levels currently
+ # 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'
From 69f21514b91796b219284ec761f267887eec9be3 Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Thu, 14 Jul 2016 22:43:49 -0700
Subject: [PATCH 26/58] Add Lodash to iframe content
---
app/assets/web-dev-iframe.html | 3 +++
1 file changed, 3 insertions(+)
diff --git a/app/assets/web-dev-iframe.html b/app/assets/web-dev-iframe.html
index d3349ecc3..39bef5aca 100644
--- a/app/assets/web-dev-iframe.html
+++ b/app/assets/web-dev-iframe.html
@@ -5,8 +5,11 @@
+
My CodeCombat Website
+
+
From c44c16e5d2cb28ddd1977a4cce027694d8abdfc5 Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Fri, 15 Jul 2016 00:40:32 -0700
Subject: [PATCH 27/58] Started implementing web-dev goals
---
app/assets/javascripts/web-dev-listener.js | 76 +++++++++++++++++++++-
app/lib/world/GoalManager.coffee | 15 ++++-
app/schemas/models/level.coffee | 3 +
app/views/play/level/PlayLevelView.coffee | 2 +-
app/views/play/level/WebSurfaceView.coffee | 4 +-
5 files changed, 93 insertions(+), 7 deletions(-)
diff --git a/app/assets/javascripts/web-dev-listener.js b/app/assets/javascripts/web-dev-listener.js
index 41ce0788b..03842912c 100644
--- a/app/assets/javascripts/web-dev-listener.js
+++ b/app/assets/javascripts/web-dev-listener.js
@@ -14,18 +14,20 @@ function receiveMessage(event) {
switch (event.data.type) {
case 'create':
create(event.data.dom);
+ checkGoals(event.data.goals);
break;
case 'update':
if (virtualDOM)
update(event.data.dom);
else
- create(event.data.dom);
+ create(event.data.dom);
+ checkGoals(event.data.goals);
break;
case 'log':
console.log(event.data.text);
break;
default:
- console.log('Unknown message type:', event.data.type);
+ console.log('Unknown message type:', event.data.type);
}
//event.source.postMessage("hi there yourself! the secret response is: rheeeeet!", event.origin);
@@ -45,3 +47,73 @@ function update(dom) {
changes.reduce(deku.dom.update(dispatch, context), concreteDOM) // Rerender
virtualDOM = event.data.dom;
}
+
+function checkGoals(goals) {
+ 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)
+ });
+ console.log('HTML', goal.id, '-', goal.name, '- succeeds?', success)
+ });
+}
+
+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;
+}
diff --git a/app/lib/world/GoalManager.coffee b/app/lib/world/GoalManager.coffee
index 378068507..4d331dca3 100644
--- a/app/lib/world/GoalManager.coffee
+++ b/app/lib/world/GoalManager.coffee
@@ -39,6 +39,7 @@ module.exports = class GoalManager extends CocoClass
subscriptions:
'god:new-world-created': 'onNewWorldCreated'
'level:restarted': 'onLevelRestarted'
+ #'tome:html-updated': 'onHTMLUpdated'
backgroundSubscriptions:
'world:thang-died': 'onThangDied'
@@ -114,7 +115,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) ->
@@ -220,6 +221,11 @@ module.exports = class GoalManager extends CocoClass
return unless linesAllowed = who[thang.id] ? who[thang.team]
@updateGoalState goalID, thang.id, 'lines', frameNumber if linesUsed > linesAllowed
+ #checkHTML: (goal, html) ->
+ # console.log 'should run selector', goal.html.selector, 'to find element(s)'
+ # console.log ' ... and should make sure that the value of', check.eventProps, 'is', _.omit(check, 'eventProps') for check in goal.html.valueChecks
+ # console.log 'should do it with cheerio', window.cheerio
+
wrapUpGoalStates: (finalFrame) ->
for goalID, state of @goalStates
if state.status is null
@@ -264,7 +270,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 +297,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)
@@ -314,6 +320,9 @@ module.exports = class GoalManager extends CocoClass
linesOfCode: 0
codeProblems: 0
+ #onHTMLUpdated: (e) ->
+ # @checkHTML goal, e.html for goal in @goals when goal.html
+
updateCodeGoalStates: ->
# TODO
diff --git a/app/schemas/models/level.coffee b/app/schemas/models/level.coffee
index 15d928dfa..d71d48f8e 100644
--- a/app/schemas/models/level.coffee
+++ b/app/schemas/models/level.coffee
@@ -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}
diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee
index 9fd28e1f6..05c0bdaa2 100644
--- a/app/views/play/level/PlayLevelView.coffee
+++ b/app/views/play/level/PlayLevelView.coffee
@@ -269,7 +269,7 @@ module.exports = class PlayLevelView extends RootView
@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 if @level.isType('web-dev')
+ @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: ->
diff --git a/app/views/play/level/WebSurfaceView.coffee b/app/views/play/level/WebSurfaceView.coffee
index 4b0bdc9ab..47e8939b6 100644
--- a/app/views/play/level/WebSurfaceView.coffee
+++ b/app/views/play/level/WebSurfaceView.coffee
@@ -12,6 +12,8 @@ module.exports = class WebSurfaceView extends CocoView
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: ->
@@ -34,7 +36,7 @@ module.exports = class WebSurfaceView extends CocoView
# 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}, '*'
+ @iframe.contentWindow.postMessage {type: messageType, dom: virtualDOM, goals: @goals}, '*'
@virtualDOM = virtualDOM
checkGoals: (dom) ->
From 739973cb470ded99a001d77c788106724d8fad17 Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Fri, 15 Jul 2016 09:11:36 -0700
Subject: [PATCH 28/58] Sending goal states to GoalManager and GoalStatusView
---
app/assets/javascripts/web-dev-listener.js | 37 +++++++++++++++-------
app/lib/world/GoalManager.coffee | 13 +++-----
app/schemas/subscriptions/god.coffee | 4 +++
app/views/play/level/WebSurfaceView.coffee | 22 ++++++++++---
4 files changed, 52 insertions(+), 24 deletions(-)
diff --git a/app/assets/javascripts/web-dev-listener.js b/app/assets/javascripts/web-dev-listener.js
index 03842912c..6303b18f9 100644
--- a/app/assets/javascripts/web-dev-listener.js
+++ b/app/assets/javascripts/web-dev-listener.js
@@ -1,27 +1,36 @@
// 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);
+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'
+];
function receiveMessage(event) {
var origin = event.origin || event.originalEvent.origin; // For Chrome, the origin property is in the event.originalEvent object.
- if (origin != 'https://codecombat.com' && origin != 'http://localhost:3000') {
- console.log("Bad origin:", origin);
+ if (allowedOrigins.indexOf(origin) == -1) {
+ 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);
+ checkGoals(event.data.goals, event.source);
break;
case 'update':
if (virtualDOM)
update(event.data.dom);
else
create(event.data.dom);
- checkGoals(event.data.goals);
+ checkGoals(event.data.goals, event.source);
break;
case 'log':
console.log(event.data.text);
@@ -29,8 +38,6 @@ function receiveMessage(event) {
default:
console.log('Unknown message type:', event.data.type);
}
-
- //event.source.postMessage("hi there yourself! the secret response is: rheeeeet!", event.origin);
}
function create(dom) {
@@ -44,21 +51,29 @@ 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
+ 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)
+ success = success && matchesCheck($result, check);
});
- console.log('HTML', goal.id, '-', goal.name, '- succeeds?', success)
+ 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) {
@@ -71,7 +86,7 @@ function downTheChain(obj, keyChain) {
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?
+ 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.
diff --git a/app/lib/world/GoalManager.coffee b/app/lib/world/GoalManager.coffee
index 4d331dca3..c701646b5 100644
--- a/app/lib/world/GoalManager.coffee
+++ b/app/lib/world/GoalManager.coffee
@@ -38,8 +38,8 @@ module.exports = class GoalManager extends CocoClass
subscriptions:
'god:new-world-created': 'onNewWorldCreated'
+ 'god:new-html-goal-states': 'onNewHTMLGoalStates'
'level:restarted': 'onLevelRestarted'
- #'tome:html-updated': 'onHTMLUpdated'
backgroundSubscriptions:
'world:thang-died': 'onThangDied'
@@ -87,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]?
@@ -221,11 +224,6 @@ module.exports = class GoalManager extends CocoClass
return unless linesAllowed = who[thang.id] ? who[thang.team]
@updateGoalState goalID, thang.id, 'lines', frameNumber if linesUsed > linesAllowed
- #checkHTML: (goal, html) ->
- # console.log 'should run selector', goal.html.selector, 'to find element(s)'
- # console.log ' ... and should make sure that the value of', check.eventProps, 'is', _.omit(check, 'eventProps') for check in goal.html.valueChecks
- # console.log 'should do it with cheerio', window.cheerio
-
wrapUpGoalStates: (finalFrame) ->
for goalID, state of @goalStates
if state.status is null
@@ -320,9 +318,6 @@ module.exports = class GoalManager extends CocoClass
linesOfCode: 0
codeProblems: 0
- #onHTMLUpdated: (e) ->
- # @checkHTML goal, e.html for goal in @goals when goal.html
-
updateCodeGoalStates: ->
# TODO
diff --git a/app/schemas/subscriptions/god.coffee b/app/schemas/subscriptions/god.coffee
index 1c59ec44f..98628cb19 100644
--- a/app/schemas/subscriptions/god.coffee
+++ b/app/schemas/subscriptions/god.coffee
@@ -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
diff --git a/app/views/play/level/WebSurfaceView.coffee b/app/views/play/level/WebSurfaceView.coffee
index 47e8939b6..07b7d6a94 100644
--- a/app/views/play/level/WebSurfaceView.coffee
+++ b/app/views/play/level/WebSurfaceView.coffee
@@ -20,7 +20,8 @@ module.exports = class WebSurfaceView extends CocoView
super()
@iframe = @$('iframe')[0]
$(@iframe).on 'load', (e) =>
- @iframe.contentWindow.postMessage {type: 'log', text: 'Player HTML iframe is ready.'}, "*"
+ window.addEventListener 'message', @onIframeMessage
+ #@iframe.contentWindow.postMessage {type: 'log', text: 'Player HTML iframe is ready.'}, "*"
@iframeLoaded = true
@onIframeLoaded?()
@onIframeLoaded = null
@@ -39,9 +40,6 @@ module.exports = class WebSurfaceView extends CocoView
@iframe.contentWindow.postMessage {type: messageType, dom: virtualDOM, goals: @goals}, '*'
@virtualDOM = virtualDOM
- checkGoals: (dom) ->
- # TODO: uhh, figure these out
-
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
@@ -49,3 +47,19 @@ module.exports = class WebSurfaceView extends CocoView
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 in ['https://codecombat.com', 'http://localhost:3000']
+ 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()
From 1e8977548629108e40cad7ffa1493ae2511b1fed Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Fri, 15 Jul 2016 09:53:08 -0700
Subject: [PATCH 29/58] Basic campaign mode victory modal hookup for web-dev
levels
---
app/locale/en.coffee | 3 +++
app/models/LevelSession.coffee | 1 +
app/schemas/schemas.coffee | 3 +++
app/styles/play/level/loading.sass | 7 +++++++
app/templates/courses/course-details.jade | 2 +-
app/templates/editor/level/edit.jade | 2 +-
app/templates/play/ladder/play_modal.jade | 2 +-
app/templates/play/level/modal/hero-victory-modal.jade | 4 ++--
app/templates/play/level/modal/victory.jade | 2 +-
app/views/play/level/PlayLevelView.coffee | 2 +-
app/views/play/level/modal/HeroVictoryModal.coffee | 2 +-
app/views/play/level/tome/CastButtonView.coffee | 6 +++---
12 files changed, 25 insertions(+), 11 deletions(-)
diff --git a/app/locale/en.coffee b/app/locale/en.coffee
index 416dfead9..c5c2138b7 100644
--- a/app/locale/en.coffee
+++ b/app/locale/en.coffee
@@ -1877,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"
diff --git a/app/models/LevelSession.coffee b/app/models/LevelSession.coffee
index 23b68851c..71bb70bb4 100644
--- a/app/models/LevelSession.coffee
+++ b/app/models/LevelSession.coffee
@@ -74,6 +74,7 @@ module.exports = class LevelSession extends CocoModel
wait
recordScores: (scores, level) ->
+ return unless scores
state = @get 'state'
oldTopScores = state.topScores ? []
newTopScores = []
diff --git a/app/schemas/schemas.coffee b/app/schemas/schemas.coffee
index f9be247c2..8dcd9bfb9 100644
--- a/app/schemas/schemas.coffee
+++ b/app/schemas/schemas.coffee
@@ -261,4 +261,7 @@ me.concept = me.shortString enum: [
'vectors'
'while_loops'
'recursion'
+ 'basic_html'
+ 'basic_css'
+ 'basic_web_scripting'
]
diff --git a/app/styles/play/level/loading.sass b/app/styles/play/level/loading.sass
index d5d3f4537..ab692465f 100644
--- a/app/styles/play/level/loading.sass
+++ b/app/styles/play/level/loading.sass
@@ -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
diff --git a/app/templates/courses/course-details.jade b/app/templates/courses/course-details.jade
index 7b0ac0cf7..b15e25cc1 100644
--- a/app/templates/courses/course-details.jade
+++ b/app/templates/courses/course-details.jade
@@ -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');
diff --git a/app/templates/editor/level/edit.jade b/app/templates/editor/level/edit.jade
index 5496527d0..62a4b009d 100644
--- a/app/templates/editor/level/edit.jade
+++ b/app/templates/editor/level/edit.jade
@@ -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
diff --git a/app/templates/play/ladder/play_modal.jade b/app/templates/play/ladder/play_modal.jade
index 10118e10c..ecb48f01a 100644
--- a/app/templates/play/ladder/play_modal.jade
+++ b/app/templates/play/ladder/play_modal.jade
@@ -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")
diff --git a/app/templates/play/level/modal/hero-victory-modal.jade b/app/templates/play/level/modal/hero-victory-modal.jade
index 540962ddd..345af217a 100644
--- a/app/templates/play/level/modal/hero-victory-modal.jade
+++ b/app/templates/play/level/modal/hero-victory-modal.jade
@@ -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
diff --git a/app/templates/play/level/modal/victory.jade b/app/templates/play/level/modal/victory.jade
index effaad52c..6e6b9c56b 100644
--- a/app/templates/play/level/modal/victory.jade
+++ b/app/templates/play/level/modal/victory.jade
@@ -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
diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee
index 05c0bdaa2..68e4bf4e3 100644
--- a/app/views/play/level/PlayLevelView.coffee
+++ b/app/views/play/level/PlayLevelView.coffee
@@ -376,7 +376,7 @@ module.exports = class PlayLevelView extends RootView
@loadingView.startUnveiling()
@loadingView.unveil true
else
- @scriptManager.initializeCamera()
+ @scriptManager?.initializeCamera()
onLoadingViewUnveiling: (e) ->
@selectHero()
diff --git a/app/views/play/level/modal/HeroVictoryModal.coffee b/app/views/play/level/modal/HeroVictoryModal.coffee
index 182a2ce4c..58ad2f6bb 100644
--- a/app/views/play/level/modal/HeroVictoryModal.coffee
+++ b/app/views/play/level/modal/HeroVictoryModal.coffee
@@ -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)
diff --git a/app/views/play/level/tome/CastButtonView.coffee b/app/views/play/level/tome/CastButtonView.coffee
index 6a1e2b28e..f29520726 100644
--- a/app/views/play/level/tome/CastButtonView.coffee
+++ b/app/views/play/level/tome/CastButtonView.coffee
@@ -42,7 +42,7 @@ module.exports = class CastButtonView extends CocoView
spell.view?.createOnCodeChangeHandlers() for spellKey, spell of @spells
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.
@@ -76,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) ->
@@ -113,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.
From bc5375770e80ad6353c81f7a0fe9ae23f8b26f38 Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Fri, 15 Jul 2016 10:13:45 -0700
Subject: [PATCH 30/58] Fix Mongoose at 4.5.3 while 4.5.4 has bug creating new
clans and trial requests
---
package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 85f417574..fe72f18cf 100644
--- a/package.json
+++ b/package.json
@@ -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",
From 1e640fb74c947aeac13f9e203140d2ea6198ee7d Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Fri, 15 Jul 2016 10:14:00 -0700
Subject: [PATCH 31/58] Fix CampaignView styles to cover the whole screen with
background again
---
app/styles/play/campaign-view.sass | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/app/styles/play/campaign-view.sass b/app/styles/play/campaign-view.sass
index 91900ba65..a86bd7a67 100644
--- a/app/styles/play/campaign-view.sass
+++ b/app/styles/play/campaign-view.sass
@@ -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.
From 5a688e42c7367e82841949439ba2ac82565f77d0 Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Fri, 15 Jul 2016 11:19:22 -0700
Subject: [PATCH 32/58] Slightly more flexible iframe origin checking
---
app/assets/javascripts/web-dev-listener.js | 15 ++++++++++-----
app/views/play/level/WebSurfaceView.coffee | 2 +-
2 files changed, 11 insertions(+), 6 deletions(-)
diff --git a/app/assets/javascripts/web-dev-listener.js b/app/assets/javascripts/web-dev-listener.js
index 6303b18f9..8e48688b7 100644
--- a/app/assets/javascripts/web-dev-listener.js
+++ b/app/assets/javascripts/web-dev-listener.js
@@ -7,15 +7,20 @@ var virtualDOM;
var goalStates;
var allowedOrigins = [
- 'https://codecombat.com',
- 'http://localhost:3000',
- 'http://direct.codecombat.com',
- 'http://staging.codecombat.com'
+ /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.
- if (allowedOrigins.indexOf(origin) == -1) {
+ var allowed = false;
+ allowedOrigins.forEach(function(pattern) {
+ allowed = allowed || pattern.test(origin);
+ });
+ if (!allowed) {
console.log('Ignoring message from bad origin:', origin);
return;
}
diff --git a/app/views/play/level/WebSurfaceView.coffee b/app/views/play/level/WebSurfaceView.coffee
index 07b7d6a94..e43399d95 100644
--- a/app/views/play/level/WebSurfaceView.coffee
+++ b/app/views/play/level/WebSurfaceView.coffee
@@ -50,7 +50,7 @@ module.exports = class WebSurfaceView extends CocoView
onIframeMessage: (e) =>
origin = e.origin or e.originalEvent.origin
- unless origin in ['https://codecombat.com', 'http://localhost:3000']
+ 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
From cb085d019d9691b9f1e919611eb5a19c2d92879d Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Fri, 15 Jul 2016 11:27:58 -0700
Subject: [PATCH 33/58] Update Aether version
---
bower.json | 2 +-
package.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/bower.json b/bower.json
index 962798f29..85732107b 100644
--- a/bower.json
+++ b/bower.json
@@ -32,7 +32,7 @@
"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",
"d3": "~3.4.4",
"jsondiffpatch": "0.1.8",
diff --git a/package.json b/package.json
index fe72f18cf..5b3aeff4f 100644
--- a/package.json
+++ b/package.json
@@ -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",
From 9be8151959bd22837977bdc5acb03d899ae9b2ab Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Fri, 15 Jul 2016 13:24:54 -0700
Subject: [PATCH 34/58] Don't create God for web-dev levels
---
app/assets/javascripts/web-dev-listener.js | 2 +-
app/views/play/level/PlayLevelView.coffee | 7 +++++--
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/app/assets/javascripts/web-dev-listener.js b/app/assets/javascripts/web-dev-listener.js
index 8e48688b7..60f4bb9c6 100644
--- a/app/assets/javascripts/web-dev-listener.js
+++ b/app/assets/javascripts/web-dev-listener.js
@@ -11,7 +11,7 @@ var allowedOrigins = [
/http:\/\/localhost:3000/,
/http:\/\/direct\.codecombat\.com/,
/http:\/\/staging\.codecombat\.com/,
- /http:\/\/codecombat-staging-codecombat\.runnableapp\.com/,
+ /http:\/\/.*codecombat-staging-codecombat\.runnableapp\.com/,
];
function receiveMessage(event) {
diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee
index 68e4bf4e3..7ea724428 100644
--- a/app/views/play/level/PlayLevelView.coffee
+++ b/app/views/play/level/PlayLevelView.coffee
@@ -73,6 +73,7 @@ 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-started': 'onRealTimePlaybackStarted'
'playback:real-time-playback-ended': 'onRealTimePlaybackEnded'
@@ -133,7 +134,6 @@ module.exports = class PlayLevelView extends RootView
load: ->
@loadStartTime = new Date()
- @god = new God({@gameUIState}) # TODO: don't make one of these in web-dev mode
levelLoaderOptions = supermodel: @supermodel, levelID: @levelID, sessionID: @sessionID, opponentSessionID: @opponentSessionID, team: @getQueryVariable('team'), observing: @observing, courseID: @courseID
if me.isSessionless()
levelLoaderOptions.fakeSessionConfig = {}
@@ -141,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()
@@ -253,7 +256,7 @@ 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 })
From 10ca59d10fced54f20991a9a26971f7206d3b18f Mon Sep 17 00:00:00 2001
From: Scott Erickson
Date: Fri, 15 Jul 2016 15:54:22 -0700
Subject: [PATCH 35/58] Have CourseVictoryModal used for course-ladder levels
---
app/models/Course.coffee | 7 +++++++
.../play/level/modal/progress-view.jade | 5 +++--
app/views/ladder/LadderPlayModal.coffee | 2 +-
.../level/modal/CourseVictoryModal.coffee | 19 +++++++++++++++++++
.../play/level/modal/ProgressView.coffee | 4 ++++
server/middleware/course-instances.coffee | 12 ++++++++++++
server/routes/index.coffee | 3 ++-
.../functional/course_instance.spec.coffee | 19 +++++++++++++++++++
8 files changed, 67 insertions(+), 4 deletions(-)
diff --git a/app/models/Course.coffee b/app/models/Course.coffee
index 7cd6ee20c..86705745e 100644
--- a/app/models/Course.coffee
+++ b/app/models/Course.coffee
@@ -5,3 +5,10 @@ module.exports = class Course extends CocoModel
@className: 'Course'
@schema: schema
urlRoot: '/db/course'
+
+ fetchForCourseInstance: (courseInstanceID, opts) ->
+ options = {
+ url: "/db/course_instance/#{courseInstanceID}/course"
+ }
+ _.extend options, opts
+ @fetch options
diff --git a/app/templates/play/level/modal/progress-view.jade b/app/templates/play/level/modal/progress-view.jade
index 8c754fb33..269e3c7e7 100644
--- a/app/templates/play/level/modal/progress-view.jade
+++ b/app/templates/play/level/modal/progress-view.jade
@@ -42,8 +42,9 @@
.row
.col-sm-5.col-sm-offset-2
- // TODO: Add this and rest of campaign functionality
- // button#continue-btn.btn.btn-illustrated.btn-default.btn-block.btn-lg.text-uppercase View Leaderboards
+ // TODO: Add rest of campaign functionality
+ if view.level.get('type') === 'course-ladder'
+ button#ladder-btn.btn.btn-illustrated.btn-default.btn-block.btn-lg.text-uppercase Ladder
.col-sm-5
if !view.nextLevel.isNew()
button#next-level-btn.btn.btn-illustrated.btn-primary.btn-block.btn-lg.text-uppercase(data-i18n='play_level.next_level')
diff --git a/app/views/ladder/LadderPlayModal.coffee b/app/views/ladder/LadderPlayModal.coffee
index 77a87fd2d..a60b10d08 100644
--- a/app/views/ladder/LadderPlayModal.coffee
+++ b/app/views/ladder/LadderPlayModal.coffee
@@ -26,8 +26,8 @@ module.exports = class LadderPlayModal extends ModalView
initialize: (options, @level, @session, @team) ->
@otherTeam = if @team is 'ogres' then 'humans' else 'ogres'
- @startLoadingChallengersMaybe()
@wizardType = ThangType.loadUniversalWizard()
+ @startLoadingChallengersMaybe()
@levelID = @level.get('slug') or @level.id
@language = @session?.get('codeLanguage') ? me.get('aceConfig')?.language ? 'python'
@languages = [
diff --git a/app/views/play/level/modal/CourseVictoryModal.coffee b/app/views/play/level/modal/CourseVictoryModal.coffee
index 8df142bd1..373f59977 100644
--- a/app/views/play/level/modal/CourseVictoryModal.coffee
+++ b/app/views/play/level/modal/CourseVictoryModal.coffee
@@ -44,6 +44,11 @@ module.exports = class CourseVictoryModal extends ModalView
@levelSessions = @supermodel.loadCollection(@levelSessions, 'sessions', {
data: { project: 'state.complete level.original playtime changed' }
}).model
+
+ if not @course
+ @course = new Course()
+ @supermodel.trackRequest @course.fetchForCourseInstance(@courseInstanceID)
+
window.tracker?.trackEvent 'Play Level Victory Modal Loaded', category: 'Students', levelSlug: @level.get('slug'), ['Mixpanel']
onResourceLoadFailed: (e) ->
@@ -53,6 +58,7 @@ module.exports = class CourseVictoryModal extends ModalView
onLoaded: ->
super()
+ @courseID ?= @course.id
@views = []
@levelSessions?.remove(@session)
@@ -67,6 +73,7 @@ module.exports = class CourseVictoryModal extends ModalView
progressView.once 'done', @onDone, @
progressView.once 'next-level', @onNextLevel, @
+ progressView.once 'ladder', @onLadder, @
for view in @views
view.on 'continue', @onViewContinue, @
@views.push(progressView)
@@ -104,3 +111,15 @@ module.exports = class CourseVictoryModal extends ModalView
else
link = "/courses/#{@courseID}/#{@courseInstanceID}"
application.router.navigate(link, {trigger: true})
+
+ onLadder: ->
+ # Preserve the supermodel as we navigate back to the ladder.
+ 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'
+ 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
diff --git a/app/views/play/level/modal/ProgressView.coffee b/app/views/play/level/modal/ProgressView.coffee
index 552d28fe0..d8d46a90a 100644
--- a/app/views/play/level/modal/ProgressView.coffee
+++ b/app/views/play/level/modal/ProgressView.coffee
@@ -10,6 +10,7 @@ module.exports = class ProgressView extends CocoView
events:
'click #done-btn': 'onClickDoneButton'
'click #next-level-btn': 'onClickNextLevelButton'
+ 'click #ladder-btn': 'onClickLadderButton'
initialize: (options) ->
@level = options.level
@@ -27,3 +28,6 @@ module.exports = class ProgressView extends CocoView
onClickNextLevelButton: ->
@trigger 'next-level'
+
+ onClickLadderButton: ->
+ @trigger 'ladder'
diff --git a/server/middleware/course-instances.coffee b/server/middleware/course-instances.coffee
index 362dc046c..e32f1eccb 100644
--- a/server/middleware/course-instances.coffee
+++ b/server/middleware/course-instances.coffee
@@ -140,6 +140,18 @@ module.exports =
res.status(200).send(classroom)
+ fetchCourse: wrap (req, res) ->
+ courseInstance = yield database.getDocFromHandle(req, CourseInstance)
+ if not courseInstance
+ throw new errors.NotFound('Course Instance not found.')
+
+ course = yield Course.findById(courseInstance.get('courseID')).select(parse.getProjectFromReq(req))
+ if not course
+ throw new errors.NotFound('Course not found.')
+
+ res.status(200).send(course.toObject({req: req}))
+
+
fetchRecent: wrap (req, res) ->
query = {$and: [{name: {$ne: 'Single Player'}}, {hourOfCode: {$ne: true}}]}
query["$and"].push(_id: {$gte: objectIdFromTimestamp(req.body.startDay + "T00:00:00.000Z")}) if req.body.startDay?
diff --git a/server/routes/index.coffee b/server/routes/index.coffee
index 9ee612b4e..576d56c61 100644
--- a/server/routes/index.coffee
+++ b/server/routes/index.coffee
@@ -88,7 +88,8 @@ module.exports.setup = (app) ->
app.get('/db/course_instance/:handle/levels/:levelOriginal/sessions/:sessionID/next', mw.courseInstances.fetchNextLevel)
app.post('/db/course_instance/:handle/members', mw.auth.checkLoggedIn(), mw.courseInstances.addMembers)
app.get('/db/course_instance/:handle/classroom', mw.auth.checkLoggedIn(), mw.courseInstances.fetchClassroom)
-
+ app.get('/db/course_instance/:handle/course', mw.auth.checkLoggedIn(), mw.courseInstances.fetchCourse)
+
app.put('/db/user/:handle', mw.users.resetEmailVerifiedFlag)
app.delete('/db/user/:handle', mw.users.removeFromClassrooms)
app.get('/db/user', mw.users.fetchByGPlusID, mw.users.fetchByFacebookID)
diff --git a/spec/server/functional/course_instance.spec.coffee b/spec/server/functional/course_instance.spec.coffee
index 5c00cec82..cbcbd8bb6 100644
--- a/spec/server/functional/course_instance.spec.coffee
+++ b/spec/server/functional/course_instance.spec.coffee
@@ -375,6 +375,25 @@ describe 'GET /db/course_instance/:handle/classroom', ->
expect(res.statusCode).toBe(403)
done()
+describe 'GET /db/course_instance/:handle/course', ->
+
+ beforeEach utils.wrap (done) ->
+ yield utils.clearModels [User, CourseInstance, Classroom]
+ @course = new Course({})
+ yield @course.save()
+ @courseInstance = new CourseInstance({courseID: @course._id})
+ yield @courseInstance.save()
+ @url = getURL("/db/course_instance/#{@courseInstance.id}/course")
+ done()
+
+ it 'returns the course instance\'s referenced course', utils.wrap (done) ->
+ user = yield utils.initUser()
+ yield utils.loginUser user
+ [res, body] = yield request.getAsync(@url, {json: true})
+ expect(res.statusCode).toBe(200)
+ expect(body._id).toBe(@course.id)
+ done()
+
describe 'POST /db/course_instance/-/recent', ->
url = getURL('/db/course_instance/-/recent')
From 788a14398ac8038079d6f9b7b499427e043f1ef5 Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Fri, 15 Jul 2016 16:22:33 -0700
Subject: [PATCH 36/58] Fix starting web dev levels
---
app/views/play/level/PlayLevelView.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee
index 3fb152b8d..7ba724ff9 100644
--- a/app/views/play/level/PlayLevelView.coffee
+++ b/app/views/play/level/PlayLevelView.coffee
@@ -184,7 +184,7 @@ module.exports = class PlayLevelView extends RootView
onWorldNecessitiesLoaded: ->
# Called when we have enough to build the world, but not everything is loaded
@grabLevelLoaderData()
- team = @getQueryVariable('team') ? @session.get('team') ? @world.teamForPlayer(0)
+ team = @getQueryVariable('team') ? @session.get('team') ? @world?.teamForPlayer(0) ? 'humans'
@loadOpponentTeam(team)
@setupGod()
@setTeam team
From 05706449433a2c4df3fe435b699ee71194cf45b0 Mon Sep 17 00:00:00 2001
From: Scott Erickson
Date: Fri, 15 Jul 2016 16:57:39 -0700
Subject: [PATCH 37/58] Set up a bunch of game dev, web dev playing logic
---
app/lib/LevelLoader.coffee | 2 +-
app/templates/play/level/control_bar.jade | 2 +-
.../play/level/modal/hero-victory-modal.jade | 2 ++
.../play/level/modal/progress-view.jade | 11 +++++++++++
app/views/core/CocoView.coffee | 7 +++++++
app/views/courses/CourseDetailsView.coffee | 2 +-
app/views/courses/TeacherClassView.coffee | 7 -------
app/views/play/level/ControlBarView.coffee | 16 +++++++---------
app/views/play/level/modal/ProgressView.coffee | 5 +++++
9 files changed, 35 insertions(+), 19 deletions(-)
diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee
index 297fd4b89..0ab93f5c6 100644
--- a/app/lib/LevelLoader.coffee
+++ b/app/lib/LevelLoader.coffee
@@ -83,7 +83,7 @@ module.exports = class LevelLoader extends CocoClass
@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
+ if (@courseID and not @level.isType('course', 'course-ladder', 'game-dev', 'web-dev')) or window.serverConfig.picoCTF
# Because we now use original hero levels for both hero and course levels, we fake being a course level in this context.
originalGet = @level.get
@level.get = ->
diff --git a/app/templates/play/level/control_bar.jade b/app/templates/play/level/control_bar.jade
index 2f6c56148..61607bef6 100644
--- a/app/templates/play/level/control_bar.jade
+++ b/app/templates/play/level/control_bar.jade
@@ -7,7 +7,7 @@
.levels-link-area
a.levels-link(href=homeLink || "/")
.glyphicon.glyphicon-play
- span(data-i18n=me.isSessionless() ? "nav.courses" : (ladderGame ? "general.ladder" : "nav.play")).home-text Levels
+ span(data-i18n=me.isSessionless() ? "nav.courses" : (ladderGame ? "general.ladder" : "nav.play")).home-text
.level-name-area-container
.level-name-area
diff --git a/app/templates/play/level/modal/hero-victory-modal.jade b/app/templates/play/level/modal/hero-victory-modal.jade
index 345af217a..ccd9fe1d4 100644
--- a/app/templates/play/level/modal/hero-victory-modal.jade
+++ b/app/templates/play/level/modal/hero-victory-modal.jade
@@ -127,6 +127,8 @@ block modal-footer-content
button.btn.btn-illustrated.btn-success.leaderboard-button.btn-lg(data-dismiss="modal", data-i18n="leaderboard.view_other_solutions") View Other Solutions
else if showReturnToCourse
button.btn.btn-illustrated.btn-warning.return-to-course-button.btn-lg(data-dismiss="modal", data-i18n="play_level.victory_go_home") Go Home
+ else if level.isType('game-dev', 'web-dev')
+ button.btn.btn-illustrated.btn-primary.btn-lg ...
if showHourOfCodeDoneButton
.hour-of-code-done
diff --git a/app/templates/play/level/modal/progress-view.jade b/app/templates/play/level/modal/progress-view.jade
index 269e3c7e7..725efe66a 100644
--- a/app/templates/play/level/modal/progress-view.jade
+++ b/app/templates/play/level/modal/progress-view.jade
@@ -39,6 +39,17 @@
h2.text-uppercase= i18n(view.nextLevel.attributes, 'name').replace('Course: ', '')
div!= view.nextLevelDescription
+
+ .well.well-sm.well-parchment
+ h3.text-uppercase
+ | Share this level so your friends and family can play it:
+
+ .row
+ .col-sm-8
+ input.text-h4.semibold.form-control.input-lg#share-level-input(value='lkasjdflkjasdf')
+ .col-sm-3
+ button#share-level-btn.btn.btn-lg.btn-success.btn-illustrated
+ span(data-i18n='') Copy URL
.row
.col-sm-5.col-sm-offset-2
diff --git a/app/views/core/CocoView.coffee b/app/views/core/CocoView.coffee
index 3ec65e5f0..3450ac721 100644
--- a/app/views/core/CocoView.coffee
+++ b/app/views/core/CocoView.coffee
@@ -496,6 +496,13 @@ module.exports = class CocoView extends Backbone.View
playSound: (trigger, volume=1) ->
Backbone.Mediator.publish 'audio-player:play-sound', trigger: trigger, volume: volume
+ tryCopy: ->
+ try
+ document.execCommand('copy')
+ catch err
+ message = 'Oops, unable to copy'
+ noty text: message, layout: 'topCenter', type: 'error', killer: false
+
mobileRELong = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i
mobileREShort = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i
diff --git a/app/views/courses/CourseDetailsView.coffee b/app/views/courses/CourseDetailsView.coffee
index 84b128df6..c910460e8 100644
--- a/app/views/courses/CourseDetailsView.coffee
+++ b/app/views/courses/CourseDetailsView.coffee
@@ -85,7 +85,7 @@ module.exports = class CourseDetailsView extends RootView
@levelConceptMap = {}
for level in @levels.models
@levelConceptMap[level.get('original')] ?= {}
- for concept in level.get('concepts')
+ for concept in level.get('concepts') or []
@levelConceptMap[level.get('original')][concept] = true
if level.isType('course-ladder')
@arenaLevel = level
diff --git a/app/views/courses/TeacherClassView.coffee b/app/views/courses/TeacherClassView.coffee
index 05318572a..ae30639f7 100644
--- a/app/views/courses/TeacherClassView.coffee
+++ b/app/views/courses/TeacherClassView.coffee
@@ -227,13 +227,6 @@ module.exports = class TeacherClassView extends RootView
@$('#join-url-input').val(@state.get('joinURL')).select()
@tryCopy()
- tryCopy: ->
- try
- document.execCommand('copy')
- catch err
- message = 'Oops, unable to copy'
- noty text: message, layout: 'topCenter', type: 'error', killer: false
-
onClickUnarchive: ->
window.tracker?.trackEvent 'Teachers Class Unarchive', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
@classroom.save { archived: false }
diff --git a/app/views/play/level/ControlBarView.coffee b/app/views/play/level/ControlBarView.coffee
index 895c6f0fc..bda779e4d 100644
--- a/app/views/play/level/ControlBarView.coffee
+++ b/app/views/play/level/ControlBarView.coffee
@@ -97,13 +97,7 @@ module.exports = class ControlBarView extends CocoView
@homeViewArgs.push leagueType
@homeViewArgs.push leagueID
@homeLink += "/#{leagueType}/#{leagueID}"
- 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.isType('course')
+ else if @level.isType('course') or @courseID
@homeLink = '/courses'
@homeViewClass = 'views/courses/CoursesView'
if @courseID
@@ -113,8 +107,12 @@ module.exports = class ControlBarView extends CocoView
if @courseInstanceID
@homeLink += "/#{@courseInstanceID}"
@homeViewArgs.push @courseInstanceID
- #else if @level.isType('game-dev') # TODO
- #else if @level.isType('web-dev') # TODO
+ else if @level.isType('hero', 'hero-coop', 'game-dev', 'web-dev') or window.serverConfig.picoCTF
+ @homeLink = '/play'
+ @homeViewClass = 'views/play/CampaignView'
+ campaign = @level.get 'campaign'
+ @homeLink += '/' + campaign
+ @homeViewArgs.push campaign
else
@homeLink = '/'
@homeViewClass = 'views/HomeView'
diff --git a/app/views/play/level/modal/ProgressView.coffee b/app/views/play/level/modal/ProgressView.coffee
index d8d46a90a..57a6b48a5 100644
--- a/app/views/play/level/modal/ProgressView.coffee
+++ b/app/views/play/level/modal/ProgressView.coffee
@@ -11,6 +11,7 @@ module.exports = class ProgressView extends CocoView
'click #done-btn': 'onClickDoneButton'
'click #next-level-btn': 'onClickNextLevelButton'
'click #ladder-btn': 'onClickLadderButton'
+ 'click #share-level-btn': 'onClickShareLevelButton'
initialize: (options) ->
@level = options.level
@@ -31,3 +32,7 @@ module.exports = class ProgressView extends CocoView
onClickLadderButton: ->
@trigger 'ladder'
+
+ onClickShareLevelButton: ->
+ @$('#share-level-input').val('alskdjfla').select()
+ @tryCopy()
From 224ad54bdda6a4af1396da697ea87782970b0706 Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Fri, 15 Jul 2016 20:03:12 -0700
Subject: [PATCH 38/58] View web dev levels. Add proper victory modal
game/webpage share links. Fix playing game dev levels. Add generic change
transition to all web-dev pages.
---
.../level/modal/share_level_parchment.png | Bin 0 -> 10917 bytes
app/assets/web-dev-iframe.html | 6 +++
app/core/Router.coffee | 1 +
app/models/LevelSession.coffee | 4 +-
.../level/modal/course-victory-modal.sass | 9 ++++-
.../play/level/modal/hero-victory-modal.sass | 27 +++++++++++++
.../play/level/modal/progress-view.sass | 12 +++++-
.../play/level/play-web-dev-level-view.sass | 18 +++++++++
.../play/level/modal/hero-victory-modal.jade | 19 ++++++++-
.../play/level/modal/progress-view.jade | 35 +++++++++++------
.../play/level/play-web-dev-level-view.jade | 10 +++++
.../play/level/PlayGameDevLevelView.coffee | 11 +++---
.../play/level/PlayWebDevLevelView.coffee | 37 ++++++++++++++++++
app/views/play/level/WebSurfaceView.coffee | 2 +-
.../level/modal/CourseVictoryModal.coffee | 5 ++-
.../play/level/modal/HeroVictoryModal.coffee | 9 ++++-
.../play/level/modal/ProgressView.coffee | 6 ++-
17 files changed, 183 insertions(+), 28 deletions(-)
create mode 100644 app/assets/images/pages/play/level/modal/share_level_parchment.png
create mode 100644 app/styles/play/level/play-web-dev-level-view.sass
create mode 100644 app/templates/play/level/play-web-dev-level-view.jade
create mode 100644 app/views/play/level/PlayWebDevLevelView.coffee
diff --git a/app/assets/images/pages/play/level/modal/share_level_parchment.png b/app/assets/images/pages/play/level/modal/share_level_parchment.png
new file mode 100644
index 0000000000000000000000000000000000000000..b76ae9fb85f0e2cba92114dcbe947616a8077ce7
GIT binary patch
literal 10917
zcmY*f1z1$g*9WAfQ@Xnw>24MfNePLS&SmKo>28oxQb43Za+PkRW632J1O%k}!}nGH
z@3%Y8-LrG&p7T3rW=_m=qqG6a*qG#)2nYz+swxW45D<`h9_W|os1MiEFkHunA4JeI
zWjTaTqm(-jw2|F&Lk~ku4KXVhCvFRC7fT?wx0CC`C+R&|3=;E}
zr2l7x*aQ8mn}?qEpCKL&lJtg}+O+a6?m${0Zb5EddMQj=T3QKr>sMmW6qJ5H{_szd
z-qypzRg8zn%gc+~OMu(O-G+zn>C>k?y!<@;{9F$sxIjM69v0qQ&LD<=nf&3S00ddN
z+qrt!xj57Q;%i~)0``!kr~f7B@8@5f9(J$(C&?M~TdfBLd4ApD;p67z`OEv^QHfu@
zVp{HYzz4~{_)GCg{4?_Z_5GG3!ShS_f57~!=|8;>s!Cx>@ccEK6sATeixC0>>y@g4
z>~n9#{alPx${wFS$d9b{S7+Zoc^a@Fz4?rc{uqNU*7^~N9oY~
zTmRSlbBHuPjCd?t083tFO72IfmXCyOQ3#SbWzZ>cRMG92YAV{t=CgkIn~zL6j%keT
zX&j9H*c*nNjd*&2!Qknt@`^jtTX2J{D=h-jJi*~eRb}O4J^ggOZ?52e-iqN!J&P0_
zr?;<~Ep&4=H|l)so_LZe8_PGE0<4h-XfM-JG#iBHrMiT7okBCLr|`;n=|XMWHD@Xm
zw(QMngI?Jr&Z~!0jZ(6=WQ@%_7%J&oV2ApO(j+;;HmjG?f!3t@6E9=Kg;;M&+npp`
zXlCX_MJx>~vc@d8gtkRp@|`H?+PUDCN;!h`L)A*6_W{27lr~WP8^h4SEB~#?L7vct
zLDYR`D$maa^iwl4!oDAu*8=QD9UM)Fjw0mm5G(ptzVTN3o&TUa7~eeaZ;m^ap0;^X
z@sZ54p}a6$a(KH~qLpNs|F;=Q=Eyq6txv)4|B|<4@9TpRJ8GRkyQ@dvsNSiYB2XQ(+YdEiTOFJ%9f$qoq-s>NO
z-JX;tbwM9BI^i50SuE?@)nb27J*;&yN=h8F3CDtxyEc0H`T6=Xs$pcrWeYjzC)$2!
zwx8QxT$-Lv8{xY|pPZT!Y#nag^`OY}k5`!^xlFHbb(@i$;Lc((X+(|}
zg+&g!Dl5xGG~L|28>6upCj`68eg{v&*ZF}uMY%wOpNjg$BA4_Z*KVceg*BlYU@)h-
zHkwA4nY85va2o@Io@m1r9ZXf
z@G22>eYD2CjAguX6$lUSqM(4kF>-V(gp;nRh3oZTTM$>FVUtvtpf(I=D1I~@LmFav
zou>E-MMS^^AS{B@y<3iT+ZLPQR3d0$K8k#Hj1O{`vbV&2vbqg}W$z-+ZhD6fzK+9j
zyA4bcpV&9)%1}WYq?*7vgO2u9!dA!z2G(}H1tR>yc9T6C=@Hg2LuI1U8*22{NY{XF
zaigoB$qopJ)9E77Yq*^c1j5&jRisn-u*{X+3oEYUq~hJ%@sIQr{FAJ(p&!-*!2JC(
z50gGFRU5zlaSeH@r_sTuRZQ2oMhky6+Zas9E>cdP+C%j+oU5i0uu!IUmrE~h30M%p
z|31;HneG4=PU-0gI6=;@4qJCvgNA0m5EnnUj?UgSDbJo=bQOzXKuikgbR@U<^21B7
zth9fgWpc@2G>M7k3j=v0@I$L0w18yNCg^pj%3X0mzaqNGExL-;o-w4utDS$-0R{E*-xv@Yho&!}hGZ$39O1L(DJ2Tp%Qc)^_gT$hS5KcNe`{6pKYR$3bIO+^vE87^wYTzJmP
z&ktq#>hmP`+N>HY_iJ%eE%&qm$GZlHHI~b1^VnEeI*jzpmR7AHAzwdsomEsX4D)~%
zZ*L!28_kDPX`o-jMZ6|D0??)~M(+;M?ADSV+uEvVtQ0uAx<+=cRWBDUO9@LR7_q8#
zFy$^_<3?n@ZzClHkKWXdh|+MB6zPyslf9Si^Qni-GC??Gj^QIE_yC8X`*(v~==p`(
zOBtUoOeE}BcSWN=>X*DQvPmnmqgGj%{Qf=I-m9W{KMiab17zC=!y0i&S3O&!J3#x=
z<7fagjiHR(R5?1z_{4fyXYPgN*pBd3EP;0mT<4z62~b|UgydY{QH;=HdNj1*T5_Rz
zSMqARnWn&~j=XuKe%&crKvS|^$6?x5oQ%#6RCJqq7*O-s-7bG~al$}oYF0-}%UlBI
zDBp4R8*k{4z?Xf9cprXXgqSWxUVI$`O@)u??#ehITr%wv84}kPKl{UtGFXok2?zw1
zPi&j&nS>~Ovt=PI5_}80(9~v%KIy%oqYVuwp>u=M?kI6LC^t#68{Pjy*D@|`)ibgA^B0tzH|TQdpZVSThA$bs8*qRspV}wo0-Dt-{-?44wd-FM@7Y2~Wzj
zatcvRnCbQSU-C*wXy9|>HoK9|ekEVmf_k#v9)+YNltK^M@}=s1Q3AFm;T5;%w^w}M
zclS$&J_lrT&Q4Fuue5C0)Sn5^lnU8=%Bi0GWOBQc`q>yNw%J}oRH}HyV@PDavv}Pc
z!~L#n%jW*s=h*Fjs%ITJ9TbDgHxoxY;uTyknkLPt+r`c@!%hxFZe^ANw$^j9V5vAp
zd{b2>q?&KI#K~My2QCpu
zz45Wr36R(A+=)O{Ru(4o6cyIUAV5kw0=$$4vpd8_GVw$%(pVaNS6BC9
z$uO?8Ym^(MIl>*2I2C+$Q(|Rgli%8jepyH1T*z^O+T7@A$&y-4KY;VkYQYmc#X-8#
zP0r}-@8+9^1bH-z*F!-ig(TE@1U@x>9^mtDvyfB5WaU`(%NjMh&!0x)Kc>)iDkCS7
zX6(q(a7b0b1+4*s)KRA}ejzD$tE4l_SA6Vp9?w5S^HLFiz;~8$$`hNm)KZU{AFno>
z$lB*4CQf^9s@vxUU-CD|hfoYVPNp2V(lZxZVIo)9y@MpQh~aKtr+zv*K8dmx`|;$t
zX}r@hQJ}=@5oTUJME0sgvMX6DBl@_r-^O_a4s0g+Pt3F*F#Bx+#hdpj
z4}8G>gzVAE7j729fOmro+y2cpm`?O#9G0pp#d|KfH#nMNc+G}Ba?Xu*xO>}tm6nxj1?Sc{1fix~Y4-bG2F&Y@3dUM6%~{
z!6pmU+k_(n?e9qud7R2$w7%7(&av>w9aJPr@PW?F&USSUXUCNbxJ?30X#vHwB!FjR
zSJy0CIBqSj`TZWzc%Ej9gk&NxBt(`F5A3E`sjtMOh^gSfA;Nvnz{^*Jt#$EQdKBq5
zu*Np=lkF6W?JRiH#I@6nX+q`;?+X(#r%TJLjlNNBi@^wjrzn!#$N8$WflXFXhT$
zf*2u68gFp6Gjf4eg^uTBu!W2ugUx;)IP{}T?y6dL#dHy&lfr1y_&lu
zow&Jt>^->Pxlr#Wd2b90c5c}Wn!1{|;ZC^rF;eA;wyLpvgxS=u`2Kz?ie#aF^VA#0
zWCGHa%)NaN^!R#N!8|q7#f5$N=->3)_0~r^YN5VNeBrD!$)eN3p!IYrkM$7<;G#Sg
z@Wxu@c}L{tF60~q-3l93{l5y;BXP;P^FCF+`i;mIxKF*I#y}EczQEHAEEs<|?X_m!a
ziNg5|(f+Fs0I79(No&@`tMzK>vZBv!a%Ps0{|c5qo60(qcRXh;*NP*Y>neud3%~CP
zbK@B=9W`QyiPxHp6Fx5Ox6|2i1^~R*xgdZ!@WP;;z3nwkp!XwF&r7|*EqNNA&WjGT
zJOFwE_J8%7U~t`71EofFIk@Z<_2n(dNZ)0&?!bobWh-OeX_7R=kWf)XFf#O>FN0Xx
zZ_n;~ZfA19&Ydc?nP%Q#yAhTRb-+&+mwCku1+=F%O)IB{HzvuVWGy7cpK@zYMYlw+
zO7ri6A(cy?n5*wcQg(f~o2Lb1oZ{C0m%1S!t7Uvy3Hq`AAf?rN^|{IY7rcQFNA0*b
zW==z#FMVD0e7f(VezYC(@Dv1H&*qO86ebiW1nwa*MP}sqtKIap?pHwwbwB+GZ5_Qx
zo;_7@s}1rHFOOQEBwjgRJs{AaOGc@QS0LaLriFDgEAXFf>+Y@}b=M0wrd+&!Z198^
z@zE6G?{7%Kr$?=4d+_rScEaVSgmjCuG`G=JA9r`$&X3L+asYx`8IYgYr>~cAgATD5
z?!F5f*IFA%L>jV22Dq7Mb)Uz!7bv!b()YCJ-dNk=M_C9ycRa7~$XT@%J
ztsW6yr_Jry;Yzz9mGtyS+qK)(59#HSz$<4uIy~Y3S|vLAOH>~ZBVWEYOekG|>or&j
za+Bs#uCv;-LN1i{?ONeURqMqwF}s0hEywZslgDF<%9*Eu_`PN(zQ<9Snl*rpLPwjqFyE
z7q3~uvyc!KmC=&Cq#z*Xk)Mabx9Y1qzP}KeSpDWIhlk&laW}#;$K#v#CU@?4w*T*T
z7DM7l=K^p1
zc$U2-+CJTa-mc+(#>fPux}(FHmCc%0Sh?m5&^Uqz#pcU0bXy=BERl}@aRW~bQ}5vS
zcSA=}X+Q|N^{vwT;JLdiK+6wubW0ZTkD-6PtPCGvVu%jkd+)8w?Q8o2>{D9%pdcE_
z>%IGliQ-arWG=3tcS+ijjn4Bbj}yeg>VBT8Twq>Pi?LqMN9*hqr{^ij_{sK
zE+}ApQU_#NSpzJaEv!?GWs2-t<843e1PziXILdVkcVEWY+sL6r3IxKX{sRF4oronQ
zMk)aCnsuhru-nbRM)~y|F%o@x6Xc5JbW)){aRy5BRQHWXf#>vfuW^Y`e&p_|jwEG*
zI#xC?b#5ezmIR7hgNXkEuLeWa#(}dhpSfUelhWC(3->>j+B;EnL6O-M^!01%*imXa
z=Ek43Cbi!*GoTx;G)5qH(>pIxg;2k5={t&UZ;4badpx^Fz+PVC2SMkeF{|v2h<4i1
zcZ$?}Bv3q&((uvw5@ekJGCx80|9PWlUvC&R0C-aqX;x6&kZ$;TUoTnXSY<71QaM_DZi(@~
zy+o_cj;w%Y7SLm}BQD*-lm{G-UPzUb00x}rJ@VL}m9CP)r^3y2V>*9tIh$lz>N*V<
zkBBIux!gat*DbO1gEir@pqo$u(k-#}1$}4i&264Vn4a&9H^(>7r2ls*7t70-#e&}I
zZ0E4-j{98>4bs0m-aH}Q?~T3xsV=2RADat^F)cN30S{ziMLDo!xj2{4W*h#E^BbYT
zZIbz8=28=}qU!NqS|DN>qf#KBUVoRxHiorz7qUlwb30BZs9=^x%^lW)I1mL_Uf0dM?IoQ5`seLCDbE8Pc7nqC;mBA7elPh0T#nxxb1ty!E;++uiAT$E#dLh!FsM^TXt|
zdS2;Vvu=0QO@|uk)As4BYbUCcIbALchRy-`6@tU>9MTnzsk-#RXn(VpHw1_8u-(NV
zxw^K19Yvh4FRl(uRSN`;cTAL{b43E0O*GCW@p7MaX`=6H
z^dDXjB&4wKidG~4iPEfCNX^@CI#u|d>GUI)8$G$tL^+HTxfhYVUEK11R-8BTa0=ny
zqQkJ_mZ$it#M^Dify^pthhKrHE9B2!*B}V-Umz4$9%p6~7uB@G!>f2b(8%dZ#rDc-
zMaOT2ljCMo@;WW9rcIU=G(x384kPhfAL3raNUMvj5{#y7{*5KX4uEibCCKxoQ}a&w
zLb5Ss&e~uZIAkR4`yrzQs5>apgLp>`{sefH{P}pLLtXmrj#T$m8s2+O`87)pB(y&Z
zl-CD&qXr}ul#)T1Q$~alIndoQmLtMube=+WtroHFQNotL7b({6vFB-;o7+1oFV%cL
z%`w|T{YdLi8WAEvc*tv+t{#nbxA24#orpZd@w7r<$!?g1Qnt-R!#_3_b7--bKKg$#
zKRPv-G*`QbHL-F>^QEKC9$ka*;$R9y9^B+5m?|9YILgC%Rk|JhVxGu$Dp)lAY#W|=s#ugU$zi3!
z^=G`y2~(@TyQ_~7TYjnhB5fj5$l~d4<`!YIau(1SAM1}hp}j}`xtK)pgmpBC42G2=
zN88RiLz-j;P;+-B_^WypQzAT`vfSG;=5dWC79#HS@|cLX-4@$S&CX_b5q{0@9Qwhv
z5+ZKDv1A{C4XFP(eWnzx8y~$uM2btYf&5#ZTAECUjYgMXPqA@bG|$RIYE}Pi^t|V}
zze*CrZ*i=QNG-}^T5qv3wKmW=EvX)=kp`(sR%$AlGlKokKSM(Q9*LD;KS;CFea@8j
zZE?5QF0)Ly=c~-SqH4Oj|K~9@zf70R1&=7$&0=m)|AXUQB>IMwW%`RPK9(2GZFnu(
z7}mV`-&7xf6pe*G9F+Yo3EmL)dx<#1uaOA>$oQ{=IS{>cI8R^9y0v>qQs(^?5vD=A
zvLq_#MwbzYQ92^ulQAYyI;=~H^<0)P!&Opl<~Lm07f96g%8fbVA}Y1LWk{X_*1eLLr(1}*TfJY7WuF^ls4^|mbZ
z4Ktn*jd2T_KZyUqor;7gy}P}zfkuPa$VOtb4vj0;MoU!w$GRbw%!${*M$UrCiVDuk
z41(xHScuZIo2f&J9ouxHTac-CloU_KKa(Z%vK*u6Lu45MZxk148Qr>??Tmyj9_g=>
zphQy2Q*N3q7m%CqVV`3qN3lXj(`^}`w+$cHiUngOdO<%239
zsgbRblf#F!@$X#z+6?cu(KBzjb9tT`_~u!**=A^OIX*79;aY?}|C
z#V)C}=7~U5O?&8qNA;{qj0C?IXh|f3;1N+spPc}~p*D{u)T4&&tVhZtBEidgBQE#<
z!I7mc35;P7=tuc~=J%%K4cDy6V0FX#(=GEJum4=4^TJ#CeX8IDpBYl%T`~yl&KOe8
ze#YNl>v(z@zi~T<`p2vYh;)fAxiDfd+YEqd}cvUr^L(+cj&{YE4uwJ2ElTu?bLrvzg43>F{&QD#gS>jt-GA
z*7>-}Tkgw8B=Ga!bX>dkw-@4`FW2iQJAbyXNQ9OmK#blm(W8()BHUwUsTq&=ztzyA
ziXir0bf^Itv}v#F*y|YWM9~z)WXO^T{CAlb&?mY~V^(TEpDunQkkacf$4v{bdV@i?
z`*#A#&^KOq*oX>wKA=Hro>d>#%^_j-{Oiabh*V5O{2uNHZ5^ZewA{+39&Hf*G5kX@
z_cnMypI`^h#yp3S=CLoc^}mJuP67Rke}Muf`TXB-`jGysXd&<{6x)PK{z8DqvU+^U
zN)XlERP(G2S$n4EMPvUwE8^ry5|9{_oqiqTNgu827+q1$PArt|6b(vnClZ}ei;+#LjZQw7TFke%sAz$ADpBt<^8H2xXygdSc)Sy9P-Hqbl
zz}NNPS!H&EC2k2Zv+-nE&ThGA^@C|4S6Ov1xI#9GvTUF*1%Qm}>~enCaH`tBq$K!x
zP2{JaKh?-7w+mgptp!O9)#+%d+J#hk)Wj6!!TVU>e>n2A{4D#3rR}4LGTRx<$|!#QF&RFqyV+B54)nX~9hyJ6_`&yWUo
zF>HjHX70y7xA$(}irQ&;P!LAFX;&6hR2~HD>YKesO&J^zhaA}1=
znL72a%%o}H=hxFM^#RSFLqcSe6B>x!v(wcBndu#!ky&7|+-@ps(Q}np$ZVA4C5R-{
z?#c`R_UOemQDyr<4t7A&znt=iYjbb>D?auxH#nj3
zx_PW;z=C;oA&XKCYdfpcjPEE*6ibegH-gJK*mn)a*;co3)8WsbU>~*6?MvQoywc5y
zEOQklXjj!B7{Fi|u$pFpljcwEGl$`)8c)%p7Ds;$svpZHj)&P<42&pZ@Zwjw8N0)nd!3o^hk2
zY8$ub`N>2@RR;iS78qyd14KJLJ)ex;RJYq4)ZI=kR#(Y(26lDUI)2~ZEi{X-Yii2(
z^##r=^A~Wa&y$*a?PXNI*H#I0%Y*kbkbWy0l9rh;k~lde7=;IKl{h$Q`A#iYYDrQaegwP
zl*gW#Jnz1_M$TUL?ZQfxr>FxrFnrzC^~HP!Fs2qIuFzSRnnPvDG&Rh*-E;Csypi+@
z3nO-5<*+{KpBJntx|=3s4QV_a95ETqbvHv7%o?*OK_AWLINPYbx7}Lc%Msge56m?4
zuQrqga!X#G4-lr@-7W65qs>d1h~<8yN?49umG}~Oau9e=DG3QIB-eQ>*_x%F*BEQ&
zw~y^>18=ocJ}u@fy8CEYlUg2%NSr~^qIME}L5$CdlralDdBmOc#mHe)+tGdPWU&sH
za8NACs>W&Qb|`-`iiNTixoB=M|L7~R5m3h&>lzJp4%uFX3-}`MuVpj
zqyEUE7Z4L29RP6~&%A2(E?oHOyeb84;benEG@D&Iiy-~hW)#54Crhj^PGm{xF)6jI
zc^fQwqwBb(GM~3tJSuYz+kGW#R29PR3n$r{o1WHm7`>r&nyWVA^Q2}7xH8YV$DfXG
zY>1fyj&r~akX@8*78<9>&}K|AZi*TzkD<~P0W-`r
zAfGoO+f7;$lEeclqBe;#zOayUu6gQkES;?Tjo+`;{btkQzKj^XfLqA
zb*h#bPET_6ySLdzV!8dSt!=fuyx_La6^^raGpaEz?WLH%XqC?#h|ksNoQVht>4`Ko
zgn8nHZ3)gCWh-`+1+?~OZ5M&Ns$DdHCTsS}hu#DQXE;LTBU;~NA@GE*Y}cfiTLnch
z1ZIylo92G0m;SO@7ddo=-jI7MEyW2k0tFcNn=TmRh=|3hoE;^Erz&rm_^isjti^Dx
zgbp+us7h6}(WZ_(=Jb2I=*M|>2D)A8+pTQ0I1pes>xkSJU#%d2`q{<&fbNWC+LNr8
zJrH@N7SiswyVwkRR9RUmUq@l{ajQ8hK8?5M`)6F+r{Bhpe1$J#%r7H^gh#O{X3`cv
zR9cQr`@41ydCe;b$Mf=^59T+2b>0$97T1nyObcqr`E=@7Hs6UI^vED*IvjZT!ox~1
z7867VCeoxs35r$26k6w3pc8zjQ0G5S_SRmlt8^i+6XucPK`5hyirmh_VO-s
zF=sql`&qHp!D2%+DxzkcoFsk3!xO(a_lfn<8TBRp;vp)cu?PBMT*PgGw
z*91{eBrtUu1K*yh`{8=lHZmH7?i$s-Lecb7Aa`;u@=>BBsR{2X{2V@QSMy~vyt1or
zC^Df1sY?kC@F^tX4*8O^MMfIm6J`Dh@R=-9QJV-T+0+q;IMz0H`()sh9daaQcfM$L
zW}76ebw#(#voH7T#Ve4wB_jld+vQQhe4bmToV#Ss6FZR$Rm_!>x$jDK5m4wUKGgOo
z>nv#?ssXFwHK#yYmW7^MYa(pSN$Fd)o?kW|ab4x6*G9n2)Mqp1H|xE{@z==G5mj5l
z1ry`vrA2gf1vied<9TWjTdX3CE4GL7}+rGP8|>Bq$`*=&o5r>FcQ1EIIUc!`AV{LIvF}
z7=aG#y3G&A*W5&FpG4z+y(@Qg0wtpm9}>3bgaRng#s-XgNdtbes&|qZ_3LFfW^vwq
z#Je;mIdmvPBe&`1rZsk%>k#3Zd=)>gq^>l=ITU%QI6WknXkJrDeHL?Al>rZ;mm7jtoTV9IINhqu8CYZ{A?xha-AWDF
zl-8>*UPb1UW5K=_E-jw#u}qpd&2PQ1RzQ;IyV+@jWT0UxoSZ2t8oZ(5$vCaK+T&>Y
z*shySI;?G2NSfP|d(i?yhqEXnY
vn?#-SZ@Kf%1LK_*`%
+
+
diff --git a/app/core/Router.coffee b/app/core/Router.coffee
index de3530738..153ab6248 100644
--- a/app/core/Router.coffee
+++ b/app/core/Router.coffee
@@ -131,6 +131,7 @@ module.exports = class CocoRouter extends Backbone.Router
'play/ladder': go('ladder/MainLadderView')
'play/level/:levelID': go('play/level/PlayLevelView')
'play/game-dev-level/:levelID/:sessionID': go('play/level/PlayGameDevLevelView')
+ 'play/web-dev-level/:levelID/:sessionID': go('play/level/PlayWebDevLevelView')
'play/spectate/:levelID': go('play/SpectateView')
'play/:map': go('play/CampaignView')
diff --git a/app/models/LevelSession.coffee b/app/models/LevelSession.coffee
index 71bb70bb4..36812462e 100644
--- a/app/models/LevelSession.coffee
+++ b/app/models/LevelSession.coffee
@@ -96,8 +96,8 @@ module.exports = class LevelSession extends CocoModel
generateSpellsObject: ->
{createAetherOptions} = require 'lib/aether_utils'
aetherOptions = createAetherOptions functionName: 'plan', codeLanguage: @get('codeLanguage')
- spellThang = aether: new Aether aetherOptions
- spells = "hero-placeholder/plan": thangs: {'Hero Placeholder': spellThang}, name: 'plan'
+ spellThang = thang: {id: 'Hero Placeholder'}, aether: new Aether aetherOptions
+ spells = "hero-placeholder/plan": thang: spellThang, name: 'plan'
source = @get('code')['hero-placeholder'].plan
try
spellThang.aether.transpile source
diff --git a/app/styles/play/level/modal/course-victory-modal.sass b/app/styles/play/level/modal/course-victory-modal.sass
index 8e624e3c3..a67e783d3 100644
--- a/app/styles/play/level/modal/course-victory-modal.sass
+++ b/app/styles/play/level/modal/course-victory-modal.sass
@@ -9,6 +9,9 @@
padding-top: 0
width: 750px
+ @media screen and ( max-height: 625px )
+ margin-top: -50px
+
.modal-content
position: relative
margin-top: -251px
@@ -55,10 +58,14 @@
top: 80px
margin-top: 80px
+ @media screen and ( max-height: 650px )
+ padding-top: 10px
+
.well-parchment
margin-top: 20px
-
+ @media screen and ( max-height: 675px )
+ margin-top: 0
html.no-borderimage
diff --git a/app/styles/play/level/modal/hero-victory-modal.sass b/app/styles/play/level/modal/hero-victory-modal.sass
index 692eef2bf..e76f3140d 100644
--- a/app/styles/play/level/modal/hero-victory-modal.sass
+++ b/app/styles/play/level/modal/hero-victory-modal.sass
@@ -298,6 +298,33 @@
height: 100%
position: absolute
+ #share-level-container
+ width: 709px
+ height: 96px
+ background: transparent url(/images/pages/play/level/modal/share_level_parchment.png)
+ position: relative
+ text-align: left
+ padding: 12px 20px 0 20px
+ text-align: center
+
+ .share-level-label
+ color: rgb(103, 92, 76)
+ text-transform: uppercase
+ font-weight: bold
+ font-family: $headings-font-family
+ font-size: 18px
+ margin-top: 13px
+ line-height: 18px
+ text-align: center
+
+ #share-level-input
+ font-size: 12px
+ margin-top: 8px
+
+ #share-level-btn
+ width: 100%
+ margin-top: 7px
+
//- Footer - other stuff
diff --git a/app/styles/play/level/modal/progress-view.sass b/app/styles/play/level/modal/progress-view.sass
index 61d9f89c3..096d0766e 100644
--- a/app/styles/play/level/modal/progress-view.sass
+++ b/app/styles/play/level/modal/progress-view.sass
@@ -4,10 +4,18 @@
color: black
margin-bottom: 5px
- p
- margin-top: 30px
+ .next-level-description
+ p
+ margin-top: 30px
.course-title
white-space: nowrap
text-overflow: ellipsis
overflow: hidden
+
+ #share-level-input
+ font-size: 12px
+ margin-top: 5px
+
+ #share-level-btn
+ width: 100%
diff --git a/app/styles/play/level/play-web-dev-level-view.sass b/app/styles/play/level/play-web-dev-level-view.sass
new file mode 100644
index 000000000..ddba9ee6e
--- /dev/null
+++ b/app/styles/play/level/play-web-dev-level-view.sass
@@ -0,0 +1,18 @@
+#play-web-dev-level-view
+ #web-surface-view
+ position: absolute
+ top: 0
+ right: 0
+ bottom: 0
+ left: 0
+ z-index: 0
+
+ #info-bar
+ position: absolute
+ right: 0
+ bottom: 0
+ left: 0
+ height: 100px
+ z-index: 1
+ background-color: transparent
+ text-align: center
diff --git a/app/templates/play/level/modal/hero-victory-modal.jade b/app/templates/play/level/modal/hero-victory-modal.jade
index ccd9fe1d4..865198358 100644
--- a/app/templates/play/level/modal/hero-victory-modal.jade
+++ b/app/templates/play/level/modal/hero-victory-modal.jade
@@ -108,6 +108,23 @@ block modal-footer-content
.total-count#gem-total 0
.total-label(data-i18n="play_level.victory_gems_gained") Gems Gained
+ if view.shareURL
+ #share-level-container
+ span.share-level-label
+ span(data-i18n='') Share this link to invite your friends & family to
+ span= ' '
+ a(href=view.shareURL, target='_blank')
+ if view.level.isType('game-dev')
+ span(data-i18n='') play your game level
+ else
+ span(data-i18n='') view your webpage
+ .row
+ .col-sm-9
+ input.text-h4.semibold.form-control.input-md#share-level-input(value=view.shareURL)
+ .col-sm-3
+ button#share-level-btn.btn.btn-md.btn-success.btn-illustrated
+ span(data-i18n='') Copy URL
+
if me.get('anonymous')
.sign-up-poke.hide
.sign-up-blurb(data-i18n="play_level.victory_sign_up_poke") Want to save your code? Create a free account!
@@ -127,8 +144,6 @@ block modal-footer-content
button.btn.btn-illustrated.btn-success.leaderboard-button.btn-lg(data-dismiss="modal", data-i18n="leaderboard.view_other_solutions") View Other Solutions
else if showReturnToCourse
button.btn.btn-illustrated.btn-warning.return-to-course-button.btn-lg(data-dismiss="modal", data-i18n="play_level.victory_go_home") Go Home
- else if level.isType('game-dev', 'web-dev')
- button.btn.btn-illustrated.btn-primary.btn-lg ...
if showHourOfCodeDoneButton
.hour-of-code-done
diff --git a/app/templates/play/level/modal/progress-view.jade b/app/templates/play/level/modal/progress-view.jade
index 725efe66a..82edb9a6e 100644
--- a/app/templates/play/level/modal/progress-view.jade
+++ b/app/templates/play/level/modal/progress-view.jade
@@ -38,18 +38,31 @@
span :
h2.text-uppercase= i18n(view.nextLevel.attributes, 'name').replace('Course: ', '')
- div!= view.nextLevelDescription
+ div.next-level-description!= view.nextLevelDescription
- .well.well-sm.well-parchment
- h3.text-uppercase
- | Share this level so your friends and family can play it:
-
- .row
- .col-sm-8
- input.text-h4.semibold.form-control.input-lg#share-level-input(value='lkasjdflkjasdf')
- .col-sm-3
- button#share-level-btn.btn.btn-lg.btn-success.btn-illustrated
- span(data-i18n='') Copy URL
+ if view.shareURL
+ .well.well-sm.well-parchment
+ h3.text-uppercase
+ if view.level.isType('game-dev')
+ span(data-i18n='') Share This Game
+ else
+ span(data-i18n='') Share This Webpage
+ p
+ span(data-i18n='') This link will let your friends & family
+ span= ' '
+ a(href=view.shareURL, target='_blank')
+ if view.level.isType('game-dev')
+ span(data-i18n='') play the game
+ else
+ span(data-i18n='') view the webpage
+ span= ' '
+ span(data-i18n='') you just created.
+ .row
+ .col-sm-9
+ input.text-h4.semibold.form-control.input-lg#share-level-input(value=view.shareURL)
+ .col-sm-3
+ button#share-level-btn.btn.btn-lg.btn-success.btn-illustrated
+ span(data-i18n='') Copy URL
.row
.col-sm-5.col-sm-offset-2
diff --git a/app/templates/play/level/play-web-dev-level-view.jade b/app/templates/play/level/play-web-dev-level-view.jade
new file mode 100644
index 000000000..9d180c707
--- /dev/null
+++ b/app/templates/play/level/play-web-dev-level-view.jade
@@ -0,0 +1,10 @@
+#web-surface-view
+
+#info-bar.style-flat
+ if !view.supermodel.finished()
+ h1(data-i18n="common.loading")
+
+ else
+ h1
+ span Creator:
+ | #{view.session.get('creatorName')}
diff --git a/app/views/play/level/PlayGameDevLevelView.coffee b/app/views/play/level/PlayGameDevLevelView.coffee
index 8c20469df..481741fa3 100644
--- a/app/views/play/level/PlayGameDevLevelView.coffee
+++ b/app/views/play/level/PlayGameDevLevelView.coffee
@@ -17,7 +17,7 @@ TEAM = 'humans'
module.exports = class PlayGameDevLevelView extends RootView
id: 'play-game-dev-level-view'
template: require 'templates/play/level/play-game-dev-level-view'
-
+
events:
'click #play-btn': 'onClickPlayButton'
@@ -26,7 +26,7 @@ module.exports = class PlayGameDevLevelView extends RootView
loading: true
progress: 0
})
-
+
@supermodel.on 'update-progress', (progress) =>
@state.set({progress: (progress*100).toFixed(1)+'%'})
@level = new Level()
@@ -41,7 +41,7 @@ module.exports = class PlayGameDevLevelView extends RootView
.then (levelLoader) =>
{ @level, @session, @world } = levelLoader
- @god.setLevel(@level.serialize(@supermodel, @session))
+ @god.setLevel(@level.serialize {@supermodel, @session})
@god.setWorldClassMap(@world.classMap)
@goalManager = new GoalManager(@world, @level.get('goals'), @team)
@god.setGoalManager(@goalManager)
@@ -52,7 +52,7 @@ module.exports = class PlayGameDevLevelView extends RootView
scripts: @world.scripts or [], view: @, @session, levelID: @level.get('slug')})
@scriptManager.loadFromSession() # Should we? TODO: Figure out how scripts work for game dev levels
@supermodel.finishLoading()
-
+
.then (supermodel) =>
@levelLoader.destroy()
@levelLoader = null
@@ -74,7 +74,8 @@ module.exports = class PlayGameDevLevelView extends RootView
@state.set('loading', false)
.catch ({message}) =>
- @state.set('errorMessage', message)
+ console.error message
+ @state.set('errorMessage', message)
onClickPlayButton: ->
@god.createWorld(@spells, false, true)
diff --git a/app/views/play/level/PlayWebDevLevelView.coffee b/app/views/play/level/PlayWebDevLevelView.coffee
new file mode 100644
index 000000000..fe92963fa
--- /dev/null
+++ b/app/views/play/level/PlayWebDevLevelView.coffee
@@ -0,0 +1,37 @@
+RootView = require 'views/core/RootView'
+
+Level = require 'models/Level'
+LevelSession = require 'models/LevelSession'
+WebSurfaceView = require './WebSurfaceView'
+
+module.exports = class PlayWebDevLevelView extends RootView
+ id: 'play-web-dev-level-view'
+ template: require 'templates/play/level/play-web-dev-level-view'
+
+# events:
+# 'click #play-btn': 'onClickPlayButton'
+
+ initialize: (@options, @levelID, @sessionID) ->
+ @courseID = @getQueryVariable 'course'
+ @level = @supermodel.loadModel(new Level _id: @levelID).model
+ @session = @supermodel.loadModel(new LevelSession _id: @sessionID).model
+
+ onLoaded: ->
+ super()
+ @insertSubView @webSurface = new WebSurfaceView {level: @level}
+ Backbone.Mediator.publish 'tome:html-updated', html: @getHTML() ? 'Player has no HTML
', create: true
+ @$el.find('#info-bar').delay(4000).fadeOut(2000)
+ $('body').css('overflow', 'hidden')
+
+ showError: (jqxhr) ->
+ $('h1').text jqxhr.statusText
+
+ getHTML: ->
+ playerHTML = @session.get('code')?['hero-placeholder']?.plan
+ return playerHTML unless hero = _.find @level.get('thangs'), id: 'Hero Placeholder'
+ return playerHTML unless programmableConfig = _.find(hero.components, (component) -> component.config?.programmableMethods).config
+ return programmableConfig.programmableMethods.plan.languages.html.replace /[\s\S]*<\/playercode>/, playerHTML
+
+ destroy: ->
+ @webSurface?.destroy()
+ super()
diff --git a/app/views/play/level/WebSurfaceView.coffee b/app/views/play/level/WebSurfaceView.coffee
index e43399d95..732468af9 100644
--- a/app/views/play/level/WebSurfaceView.coffee
+++ b/app/views/play/level/WebSurfaceView.coffee
@@ -12,7 +12,7 @@ module.exports = class WebSurfaceView extends CocoView
initialize: (options) ->
@state = new State
blah: 'blah'
- @goals = (goal for goal in options.goalManager.goals when goal.html)
+ @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)
diff --git a/app/views/play/level/modal/CourseVictoryModal.coffee b/app/views/play/level/modal/CourseVictoryModal.coffee
index 373f59977..0e34d2744 100644
--- a/app/views/play/level/modal/CourseVictoryModal.coffee
+++ b/app/views/play/level/modal/CourseVictoryModal.coffee
@@ -44,11 +44,11 @@ module.exports = class CourseVictoryModal extends ModalView
@levelSessions = @supermodel.loadCollection(@levelSessions, 'sessions', {
data: { project: 'state.complete level.original playtime changed' }
}).model
-
+
if not @course
@course = new Course()
@supermodel.trackRequest @course.fetchForCourseInstance(@courseInstanceID)
-
+
window.tracker?.trackEvent 'Play Level Victory Modal Loaded', category: 'Students', levelSlug: @level.get('slug'), ['Mixpanel']
onResourceLoadFailed: (e) ->
@@ -69,6 +69,7 @@ module.exports = class CourseVictoryModal extends ModalView
course: @course
classroom: @classroom
levelSessions: @levelSessions
+ session: @session
})
progressView.once 'done', @onDone, @
diff --git a/app/views/play/level/modal/HeroVictoryModal.coffee b/app/views/play/level/modal/HeroVictoryModal.coffee
index 58ad2f6bb..2f55d546b 100644
--- a/app/views/play/level/modal/HeroVictoryModal.coffee
+++ b/app/views/play/level/modal/HeroVictoryModal.coffee
@@ -32,6 +32,7 @@ module.exports = class HeroVictoryModal extends ModalView
'click .sign-up-button': 'onClickSignupButton'
'click .continue-from-offer-button': 'onClickContinueFromOffer'
'click .skip-offer-button': 'onClickSkipOffer'
+ 'click #share-level-btn': 'onClickShareLevelButton'
# Feedback events
'mouseover .rating i': (e) -> @showStars(@starNum($(e.target)))
@@ -73,7 +74,9 @@ module.exports = class HeroVictoryModal extends ModalView
if @level.isType('course', 'course-ladder')
@saveReviewEventually = _.debounce(@saveReviewEventually, 2000)
@loadExistingFeedback()
- # TODO: support 'game-dev' and 'web-dev' (not the same as 'course' since can be played outside of courses)
+
+ if @level.isType('game-dev', 'web-dev')
+ @shareURL = "#{window.location.origin}/play/#{@level.get('type')}-level/#{@level.get('slug')}/#{@session.id}"
destroy: ->
clearInterval @sequentialAnimationInterval
@@ -499,6 +502,10 @@ module.exports = class HeroVictoryModal extends ModalView
onClickSkipOffer: (e) ->
Backbone.Mediator.publish 'router:navigate', @navigationEventUponCompletion
+ onClickShareLevelButton: ->
+ @$('#share-level-input').val(@shareURL).select()
+ @tryCopy()
+
# Ratings and reviews
starNum: (starEl) -> starEl.prevAll('i').length + 1
diff --git a/app/views/play/level/modal/ProgressView.coffee b/app/views/play/level/modal/ProgressView.coffee
index 57a6b48a5..23d5879ef 100644
--- a/app/views/play/level/modal/ProgressView.coffee
+++ b/app/views/play/level/modal/ProgressView.coffee
@@ -19,10 +19,14 @@ module.exports = class ProgressView extends CocoView
@classroom = options.classroom
@nextLevel = options.nextLevel
@levelSessions = options.levelSessions
+ @session = options.session
# Translate and Markdownify level description, but take out any images (we don't have room for arena banners, etc.).
# Images in Markdown are like 
@nextLevel.get('description', true) # Make sure the defaults are available
@nextLevelDescription = marked(utils.i18n(@nextLevel.attributesWithDefaults, 'description').replace(/!\[.*?\]\(.*?\)\n*/g, ''))
+ if @level.isType('game-dev', 'web-dev')
+ @shareURL = "#{window.location.origin}/play/#{@level.get('type')}-level/#{@level.get('slug')}/#{@session.id}"
+ @shareURL += "?course=#{@course.id}" if @course
onClickDoneButton: ->
@trigger 'done'
@@ -34,5 +38,5 @@ module.exports = class ProgressView extends CocoView
@trigger 'ladder'
onClickShareLevelButton: ->
- @$('#share-level-input').val('alskdjfla').select()
+ @$('#share-level-input').val(@shareURL).select()
@tryCopy()
From 7e4733f07e944f492a0b888aefae776ba78f6648 Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Fri, 15 Jul 2016 20:25:37 -0700
Subject: [PATCH 39/58] Hack: check HTML goals now and in a second to account
for built-in CSS transition
---
app/assets/javascripts/web-dev-listener.js | 28 ++++++++++++++--------
1 file changed, 18 insertions(+), 10 deletions(-)
diff --git a/app/assets/javascripts/web-dev-listener.js b/app/assets/javascripts/web-dev-listener.js
index 60f4bb9c6..b6fea6acd 100644
--- a/app/assets/javascripts/web-dev-listener.js
+++ b/app/assets/javascripts/web-dev-listener.js
@@ -25,23 +25,25 @@ function receiveMessage(event) {
return;
}
//console.log(event);
- switch (event.data.type) {
+ var data = event.data;
+ var source = event.source;
+ switch (data.type) {
case 'create':
- create(event.data.dom);
- checkGoals(event.data.goals, event.source);
+ create(data.dom);
+ checkGoals(data.goals, source, origin);
break;
case 'update':
if (virtualDOM)
- update(event.data.dom);
+ update(data.dom);
else
- create(event.data.dom);
- checkGoals(event.data.goals, event.source);
+ create(data.dom);
+ checkGoals(data.goals, source, origin);
break;
case 'log':
- console.log(event.data.text);
+ console.log(data.text);
break;
default:
- console.log('Unknown message type:', event.data.type);
+ console.log('Unknown message type:', data.type);
}
}
@@ -60,7 +62,13 @@ function update(dom) {
virtualDOM = event.data.dom;
}
-function checkGoals(goals) {
+function checkGoals(goals, source, origin) {
+ // Check right now and also in one second, since our 1-second CSS transition might be affecting things until it is done.
+ doCheckGoals(goals, source, origin);
+ _.delay(function() { doCheckGoals(goals, source, origin); }, 1001);
+}
+
+function doCheckGoals(goals, source, origin) {
var newGoalStates = {};
var overallSuccess = true;
goals.forEach(function(goal) {
@@ -77,7 +85,7 @@ function checkGoals(goals) {
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);
+ source.postMessage({type: 'goals-updated', goalStates: goalStates, overallStatus: overallStatus}, origin);
}
}
From 5f95a4d158e279e67c1db26aa37d0f5cfb365024 Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Fri, 15 Jul 2016 20:47:09 -0700
Subject: [PATCH 40/58] Play game-dev levels without API restrictions. Show
game button in CourseDetailsView only when appropriate.
---
app/models/LevelSession.coffee | 7 ++++---
app/templates/courses/course-details.jade | 4 ++--
app/views/courses/CourseDetailsView.coffee | 1 -
app/views/play/level/PlayGameDevLevelView.coffee | 3 +--
4 files changed, 7 insertions(+), 8 deletions(-)
diff --git a/app/models/LevelSession.coffee b/app/models/LevelSession.coffee
index 36812462e..9e845fa78 100644
--- a/app/models/LevelSession.coffee
+++ b/app/models/LevelSession.coffee
@@ -93,12 +93,13 @@ module.exports = class LevelSession extends CocoModel
state.topScores = newTopScores
@set 'state', state
- generateSpellsObject: ->
+ generateSpellsObject: (options={}) ->
+ {level} = options
{createAetherOptions} = require 'lib/aether_utils'
- aetherOptions = createAetherOptions functionName: 'plan', codeLanguage: @get('codeLanguage')
+ aetherOptions = createAetherOptions functionName: 'plan', codeLanguage: @get('codeLanguage'), skipProtectAPI: options.level?.isType('game-dev')
spellThang = thang: {id: 'Hero Placeholder'}, aether: new Aether aetherOptions
spells = "hero-placeholder/plan": thang: spellThang, name: 'plan'
- source = @get('code')['hero-placeholder'].plan
+ source = @get('code')?['hero-placeholder']?.plan ? ''
try
spellThang.aether.transpile source
catch e
diff --git a/app/templates/courses/course-details.jade b/app/templates/courses/course-details.jade
index b15e25cc1..fd3039df2 100644
--- a/app/templates/courses/course-details.jade
+++ b/app/templates/courses/course-details.jade
@@ -106,11 +106,11 @@ block content
if previousLevelCompleted || view.teacherMode || !passedLastCompletedLevel || levelStatus
- 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
+ if level.isType('game-dev')
- var levelOriginal = level.get('original');
- var session = view.levelSessions.find(function(session) { return session.get('level').original === levelOriginal });
if session
- a.btn.btn-warning(href="/play/game-dev-level/#{level.get('slug')}/#{session.id}") Play Game Dev
+ a.btn.btn-warning(href="/play/game-dev-level/#{level.get('slug')}/#{session.id}?course=#{view.courseID}") Game
td
if view.userLevelStateMap[me.id]
div= view.userLevelStateMap[me.id][level.get('original')]
diff --git a/app/views/courses/CourseDetailsView.coffee b/app/views/courses/CourseDetailsView.coffee
index c910460e8..8fb01e791 100644
--- a/app/views/courses/CourseDetailsView.coffee
+++ b/app/views/courses/CourseDetailsView.coffee
@@ -33,7 +33,6 @@ module.exports = class CourseDetailsView extends RootView
@classroom = new Classroom()
@levels = new Levels()
@courseInstances = new CourseInstances()
- @showGameDevButtons = me.isAdmin() or window.amActually # TEMP while testing game dev level system
@supermodel.trackRequest @ownedClassrooms.fetchMine({data: {project: '_id'}})
@supermodel.trackRequest(@courses.fetch().then(=>
diff --git a/app/views/play/level/PlayGameDevLevelView.coffee b/app/views/play/level/PlayGameDevLevelView.coffee
index 481741fa3..9de9fb440 100644
--- a/app/views/play/level/PlayGameDevLevelView.coffee
+++ b/app/views/play/level/PlayGameDevLevelView.coffee
@@ -9,7 +9,6 @@ Surface = require 'lib/surface/Surface'
ThangType = require 'models/ThangType'
Level = require 'models/Level'
LevelSession = require 'models/LevelSession'
-{createAetherOptions} = require 'lib/aether_utils'
State = require 'models/State'
TEAM = 'humans'
@@ -70,7 +69,7 @@ module.exports = class PlayGameDevLevelView extends RootView
@surface.setWorld(@world)
@scriptManager.initializeCamera()
@renderSelectors '#info-col'
- @spells = @session.generateSpellsObject()
+ @spells = @session.generateSpellsObject level: @level
@state.set('loading', false)
.catch ({message}) =>
From 5d26b03918bd3bba983401b9a6a9f6c7316309bb Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Fri, 15 Jul 2016 21:57:04 -0700
Subject: [PATCH 41/58] Add buttons to view game/web-dev levels to courses
views
---
app/models/Classroom.coffee | 4 +-
app/styles/courses/course-details.sass | 3 ++
app/styles/courses/teacher-class-view.sass | 14 ++++++-
app/templates/courses/course-details.jade | 9 ++++-
app/templates/courses/teacher-class-view.jade | 37 +++++++++++++++++--
app/views/courses/TeacherClassView.coffee | 6 +--
.../play/level/PlayWebDevLevelView.coffee | 1 +
7 files changed, 63 insertions(+), 11 deletions(-)
diff --git a/app/models/Classroom.coffee b/app/models/Classroom.coffee
index ef47355b0..d855f7bfc 100644
--- a/app/models/Classroom.coffee
+++ b/app/models/Classroom.coffee
@@ -74,7 +74,7 @@ module.exports = class Classroom extends CocoModel
}
getLevels: (options={}) ->
- # options: courseID, withoutLadderLevels
+ # options: courseID, withoutLadderLevels, projectLevels
Levels = require 'collections/Levels'
courses = @get('courses')
return new Levels() unless courses
@@ -86,6 +86,8 @@ module.exports = class Classroom extends CocoModel
levels = new Levels(_.flatten(levelObjects))
if options.withoutLadderLevels
levels.remove(levels.filter((level) -> level.isLadder()))
+ if options.projectLevels
+ levels.remove(levels.filter((level) -> not level.isType('game-dev', 'web-dev')))
return levels
getLadderLevel: (courseID) ->
diff --git a/app/styles/courses/course-details.sass b/app/styles/courses/course-details.sass
index b8e76e217..179164b3b 100644
--- a/app/styles/courses/course-details.sass
+++ b/app/styles/courses/course-details.sass
@@ -35,3 +35,6 @@
h1
font-size: 48px
+
+ .btn-view-project-level
+ margin-left: 10px;
diff --git a/app/styles/courses/teacher-class-view.sass b/app/styles/courses/teacher-class-view.sass
index c5d848b38..0a5ab9c0b 100644
--- a/app/styles/courses/teacher-class-view.sass
+++ b/app/styles/courses/teacher-class-view.sass
@@ -177,7 +177,7 @@
// Course Progress tab
- #course-progress-tab
+ #course-progress-tab, #student-projects-tab
.course-overview-row
margin-top: 50px
border: thin solid gray
@@ -221,7 +221,17 @@
.btn
margin-top: 6.5px
margin-bottom: 6.5px
-
+
+ #student-projects-tab
+ .student-info
+ margin-top: 5px
+
+ .student-levels-row
+ padding-top: 10px
+ padding-bottom: 15px
+
+ .btn-view-project-level
+ margin-left: 15px
// Checkboxes
.checkbox-flat
diff --git a/app/templates/courses/course-details.jade b/app/templates/courses/course-details.jade
index fd3039df2..6a0c57530 100644
--- a/app/templates/courses/course-details.jade
+++ b/app/templates/courses/course-details.jade
@@ -106,11 +106,16 @@ block content
if previousLevelCompleted || view.teacherMode || !passedLastCompletedLevel || levelStatus
- 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 level.isType('game-dev')
+ if level.isType('game-dev', 'web-dev')
- var levelOriginal = level.get('original');
- var session = view.levelSessions.find(function(session) { return session.get('level').original === levelOriginal });
if session
- a.btn.btn-warning(href="/play/game-dev-level/#{level.get('slug')}/#{session.id}?course=#{view.courseID}") Game
+ - var url = '/play/' + level.get('type') + '-level/' + level.get('slug') + '/' + session.id + '?course=' + view.courseID;
+ a.btn.btn-warning.btn-view-project-level(href=url)
+ if level.isType('game-dev')
+ span(data-i18n='') Game
+ else
+ span(data-i18n='') Webpage
td
if view.userLevelStateMap[me.id]
div= view.userLevelStateMap[me.id][level.get('original')]
diff --git a/app/templates/courses/teacher-class-view.jade b/app/templates/courses/teacher-class-view.jade
index 4df8d4acb..283e8dfbe 100644
--- a/app/templates/courses/teacher-class-view.jade
+++ b/app/templates/courses/teacher-class-view.jade
@@ -311,7 +311,7 @@ mixin courseOverview
.course-overview-row
.course-title.student-name
span= course.get('name')
- span :
+ span= ': '
span(data-i18n='teacher.course_overview')
.course-overview-progress
each level, index in levels
@@ -330,7 +330,7 @@ mixin studentLevelsRow(student)
each level, index in levels
- var progress = state.get('progressData').get({ classroom: view.classroom, course: course, level: level, user: student })
- var levelNumber = view.classroom.getLevelNumber(level.get('original'), index + 1)
- +studentLevelProgressDot(progress, level, levelNumber, session)
+ +studentLevelProgressDot(progress, level, levelNumber)
mixin studentCourseProgressDot(progress, levelsTotal, level, label)
//- TODO: Refactor with TeacherClassesView jade
@@ -438,4 +438,35 @@ mixin enrollmentStatusTab
button.enroll-student-button.btn.btn-navy(data-i18n="teacher.enroll_student", data-user-id=student.id, data-event-action="Teachers Class Enrollment Enroll Student")
mixin studentProjectsTab
- p ...
+ #student-projects-tab.m-t-3
+ if state.get('progressData')
+ .render-on-course-sync
+ .student-levels-table
+ +sortButtons
+ each student in state.get('students').models
+ +studentProjectsRow(student)
+
+mixin studentProjectsRow(student)
+ .row.student-levels-row.alternating-background
+ div.student-info.col-sm-3
+ div.student-name= student.broadName()
+ div.student-email.small-details= student.get('email')
+ div.student-levels-progress.col-sm-9
+ each trimCourse in view.classroom.get('courses')
+ - var course = view.courses.get(trimCourse._id);
+ - var levels = view.classroom.getLevels({courseID: course.id, projectLevels: true}).models
+ each level in levels
+ - var progress = state.get('progressData').get({ classroom: view.classroom, course: course, level: level, user: student })
+ - var levelNumber = view.classroom.getLevelNumber(level.get('original'), index + 1)
+ +studentProjectLink(progress, level, levelNumber, course)
+
+mixin studentProjectLink(progress, level, levelNumber, course)
+ - var colorClass = progress.completed ? 'btn-primary' : (progress.started ? 'btn-warning' : 'btn-primary');
+ - var levelName = level.get('name')
+ - var context = _.merge(progress, { levelName: levelName, levelNumber: levelNumber, moment: moment })
+ - var title = view.singleStudentLevelProgressDotTemplate(context);
+ if context.session
+ - var url = '/play/' + level.get('type') + '-level/' + level.get('slug') + '/' + context.session.id + '?course=' + course.id;
+ a(class="btn btn-lg btn-view-project-level " + colorClass, href=url, data-title=title)= levelName
+ else
+ btn(class="btn btn-lg btn-view-project-level " + colorClass, data-title=title, disabled=true)= levelName
diff --git a/app/views/courses/TeacherClassView.coffee b/app/views/courses/TeacherClassView.coffee
index ae30639f7..29ec223e1 100644
--- a/app/views/courses/TeacherClassView.coffee
+++ b/app/views/courses/TeacherClassView.coffee
@@ -174,17 +174,17 @@ module.exports = class TeacherClassView extends RootView
@debouncedRender()
@listenTo @students, 'sort', @debouncedRender
super()
-
+
afterRender: ->
super(arguments...)
- $('.progress-dot').each (i, el) ->
+ $('.progress-dot, .btn-view-project-level').each (i, el) ->
dot = $(el)
dot.tooltip({
html: true
container: dot
}).delegate '.tooltip', 'mousemove', ->
dot.tooltip('hide')
-
+
calculateProgressAndLevels: ->
return unless @supermodel.progress is 1
# TODO: How to structure this in @state?
diff --git a/app/views/play/level/PlayWebDevLevelView.coffee b/app/views/play/level/PlayWebDevLevelView.coffee
index fe92963fa..660a3b58d 100644
--- a/app/views/play/level/PlayWebDevLevelView.coffee
+++ b/app/views/play/level/PlayWebDevLevelView.coffee
@@ -34,4 +34,5 @@ module.exports = class PlayWebDevLevelView extends RootView
destroy: ->
@webSurface?.destroy()
+ $('body').css('overflow', 'initial')
super()
From 6ae89e31f13905aa710f6999e25e08868bbb1f73 Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Fri, 15 Jul 2016 22:14:25 -0700
Subject: [PATCH 42/58] Add direct link to play game/web-dev creations while
coding them
---
app/templates/play/level/tome/spell-top-bar-view.jade | 9 +++++++++
app/views/play/level/tome/Spell.coffee | 2 ++
app/views/play/level/tome/TomeView.coffee | 1 +
3 files changed, 12 insertions(+)
diff --git a/app/templates/play/level/tome/spell-top-bar-view.jade b/app/templates/play/level/tome/spell-top-bar-view.jade
index d9341fb29..cdacc2c9c 100644
--- a/app/templates/play/level/tome/spell-top-bar-view.jade
+++ b/app/templates/play/level/tome/spell-top-bar-view.jade
@@ -21,4 +21,13 @@
.btn.btn-small.btn-illustrated.hints-button
span(data-i18n="play_level.hints")
+ if view.options.level.isType('game-dev', 'web-dev')
+ - var url = '/play/' + view.options.level.get('type') + '-level/' + view.options.level.get('slug') + '/' + view.options.session.id;
+ - if (view.options.courseID) url += '?course=' + view.options.courseID;
+ a.btn.btn-small.btn-illustrated(href=url)
+ if view.options.level.isType('game-dev')
+ span(data-i18n='') Game
+ else
+ span(data-i18n='') Webpage
+
.clearfix
diff --git a/app/views/play/level/tome/Spell.coffee b/app/views/play/level/tome/Spell.coffee
index d1e119f28..6bc7bc265 100644
--- a/app/views/play/level/tome/Spell.coffee
+++ b/app/views/play/level/tome/Spell.coffee
@@ -54,6 +54,8 @@ module.exports = class Spell
supermodel: @supermodel
codeLanguage: @language
level: options.level
+ session: options.session
+ courseID: options.courseID
@topBarView.render()
Backbone.Mediator.publish 'tome:spell-created', spell: @
diff --git a/app/views/play/level/tome/TomeView.coffee b/app/views/play/level/tome/TomeView.coffee
index c35766665..b9fa26e77 100644
--- a/app/views/play/level/tome/TomeView.coffee
+++ b/app/views/play/level/tome/TomeView.coffee
@@ -122,6 +122,7 @@ module.exports = class TomeView extends CocoView
levelID: @options.levelID
level: @options.level
god: @options.god
+ courseID: @options.courseID
for thangID, spellKeys of @thangSpells
thang = @fakeProgrammableThang ? world.getThangByID thangID
From b64bcd9f02b57c2bd082256be2520b5bfcdc2aaa Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Fri, 15 Jul 2016 23:26:43 -0700
Subject: [PATCH 43/58] Use shareable false/true/'project' for different levels
of shareability
---
app/models/Classroom.coffee | 2 +-
app/schemas/models/campaign.schema.coffee | 1 +
app/schemas/models/classroom.schema.coffee | 2 +-
app/schemas/models/level.coffee | 2 +-
app/styles/courses/teacher-class-view.sass | 3 +++
app/templates/courses/course-details.jade | 2 +-
app/templates/play/level/tome/spell-top-bar-view.jade | 2 +-
app/views/courses/CourseDetailsView.coffee | 2 +-
app/views/courses/TeacherClassView.coffee | 7 ++++---
app/views/play/CampaignView.coffee | 3 ++-
app/views/play/level/PlayLevelView.coffee | 3 +++
app/views/play/level/modal/HeroVictoryModal.coffee | 2 +-
app/views/play/level/modal/ProgressView.coffee | 2 +-
13 files changed, 21 insertions(+), 12 deletions(-)
diff --git a/app/models/Classroom.coffee b/app/models/Classroom.coffee
index d855f7bfc..eef817bfb 100644
--- a/app/models/Classroom.coffee
+++ b/app/models/Classroom.coffee
@@ -87,7 +87,7 @@ module.exports = class Classroom extends CocoModel
if options.withoutLadderLevels
levels.remove(levels.filter((level) -> level.isLadder()))
if options.projectLevels
- levels.remove(levels.filter((level) -> not level.isType('game-dev', 'web-dev')))
+ levels.remove(levels.filter((level) -> level.get('shareable') isnt 'project'))
return levels
getLadderLevel: (courseID) ->
diff --git a/app/schemas/models/campaign.schema.coffee b/app/schemas/models/campaign.schema.coffee
index ff78c8263..82b0eb763 100644
--- a/app/schemas/models/campaign.schema.coffee
+++ b/app/schemas/models/campaign.schema.coffee
@@ -67,6 +67,7 @@ _.extend CampaignSchema.properties, {
adventurer: { type: 'boolean' }
practice: { type: 'boolean' }
practiceThresholdMinutes: {type: 'number'}
+ shareable: { title: 'Shareable', type: ['string', 'boolean'], enum: [false, true, 'project'], description: 'Whether the level is not shareable, shareable, or a sharing-encouraged project level.' }
adminOnly: { type: 'boolean' }
disableSpaces: { type: ['boolean','number'] }
hidesSubmitUntilRun: { type: 'boolean' }
diff --git a/app/schemas/models/classroom.schema.coffee b/app/schemas/models/classroom.schema.coffee
index 9f3e63ca7..e33563b40 100644
--- a/app/schemas/models/classroom.schema.coffee
+++ b/app/schemas/models/classroom.schema.coffee
@@ -25,7 +25,7 @@ _.extend ClassroomSchema.properties,
levels: c.array { title: 'Levels' }, c.object { title: 'Level' }, {
practice: {type: 'boolean'}
practiceThresholdMinutes: {type: 'number'}
- shareable: {type: 'boolean'}
+ shareable: { title: 'Shareable', type: ['string', 'boolean'], enum: [false, true, 'project'], description: 'Whether the level is not shareable, shareable, or a sharing-encouraged project level.' }
type: c.shortString()
original: c.objectId()
name: {type: 'string'}
diff --git a/app/schemas/models/level.coffee b/app/schemas/models/level.coffee
index f3b311245..75c31b35f 100644
--- a/app/schemas/models/level.coffee
+++ b/app/schemas/models/level.coffee
@@ -328,8 +328,8 @@ _.extend LevelSchema.properties,
replayable: {type: 'boolean', title: 'Replayable', description: 'Whether this (hero) level infinitely scales up its difficulty and can be beaten over and over for greater rewards.'}
buildTime: {type: 'number', description: 'How long it has taken to build this level.'}
practice: { type: 'boolean' }
- shareable: { type: 'boolean', title: 'Shareable' }
practiceThresholdMinutes: {type: 'number', description: 'Players with larger playtimes may be directed to a practice level.'}
+ shareable: { title: 'Shareable', type: ['string', 'boolean'], enum: [false, true, 'project'], description: 'Whether the level is not shareable, shareable, or a sharing-encouraged project level.' }
# Admin flags
adventurer: { type: 'boolean' }
diff --git a/app/styles/courses/teacher-class-view.sass b/app/styles/courses/teacher-class-view.sass
index 0a5ab9c0b..66f880ce5 100644
--- a/app/styles/courses/teacher-class-view.sass
+++ b/app/styles/courses/teacher-class-view.sass
@@ -223,6 +223,9 @@
margin-bottom: 6.5px
#student-projects-tab
+ .student-levels-table
+ margin-top: 0px
+
.student-info
margin-top: 5px
diff --git a/app/templates/courses/course-details.jade b/app/templates/courses/course-details.jade
index 6a0c57530..78953cc33 100644
--- a/app/templates/courses/course-details.jade
+++ b/app/templates/courses/course-details.jade
@@ -106,7 +106,7 @@ block content
if previousLevelCompleted || view.teacherMode || !passedLastCompletedLevel || levelStatus
- 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 level.isType('game-dev', 'web-dev')
+ if level.get('shareable')
- var levelOriginal = level.get('original');
- var session = view.levelSessions.find(function(session) { return session.get('level').original === levelOriginal });
if session
diff --git a/app/templates/play/level/tome/spell-top-bar-view.jade b/app/templates/play/level/tome/spell-top-bar-view.jade
index cdacc2c9c..30f0bf1de 100644
--- a/app/templates/play/level/tome/spell-top-bar-view.jade
+++ b/app/templates/play/level/tome/spell-top-bar-view.jade
@@ -21,7 +21,7 @@
.btn.btn-small.btn-illustrated.hints-button
span(data-i18n="play_level.hints")
- if view.options.level.isType('game-dev', 'web-dev')
+ if view.options.level.get('shareable')
- var url = '/play/' + view.options.level.get('type') + '-level/' + view.options.level.get('slug') + '/' + view.options.session.id;
- if (view.options.courseID) url += '?course=' + view.options.courseID;
a.btn.btn-small.btn-illustrated(href=url)
diff --git a/app/views/courses/CourseDetailsView.coffee b/app/views/courses/CourseDetailsView.coffee
index 8fb01e791..91d10c1f6 100644
--- a/app/views/courses/CourseDetailsView.coffee
+++ b/app/views/courses/CourseDetailsView.coffee
@@ -52,7 +52,7 @@ module.exports = class CourseDetailsView extends RootView
@supermodel.trackRequest(@classroom.fetch())
levelsLoaded = @supermodel.trackRequest(@levels.fetchForClassroomAndCourse(classroomID, @courseID, {
- data: { project: 'concepts,practice,type,slug,name,original,description' }
+ data: { project: 'concepts,practice,type,slug,name,original,description,shareable' }
}))
@supermodel.trackRequest($.when(levelsLoaded, sessionsLoaded).then(=>
diff --git a/app/views/courses/TeacherClassView.coffee b/app/views/courses/TeacherClassView.coffee
index 29ec223e1..4aefcc800 100644
--- a/app/views/courses/TeacherClassView.coffee
+++ b/app/views/courses/TeacherClassView.coffee
@@ -114,13 +114,13 @@ module.exports = class TeacherClassView extends RootView
@courses = new Courses()
@supermodel.trackRequest @courses.fetch()
-
+
@courseInstances = new CourseInstances()
@supermodel.trackRequest @courseInstances.fetchForClassroom(classroomID)
@levels = new Levels()
- @supermodel.trackRequest @levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts,practice'}})
-
+ @supermodel.trackRequest @levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts,practice,shareable'}})
+
@attachMediatorEvents()
window.tracker?.trackEvent 'Teachers Class Loaded', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
@@ -389,6 +389,7 @@ module.exports = class TeacherClassView extends RootView
not @students.get(userID).isEnrolled()
assigningToNobody = selectedIDs.length is 0
@state.set errors: { assigningToNobody, assigningToUnenrolled }
+ return if assigningToNobody
@assignCourse courseID, members
window.tracker?.trackEvent 'Teachers Class Students Assign Selected', category: 'Teachers', classroomID: @classroom.id, courseID: courseID, ['Mixpanel']
diff --git a/app/views/play/CampaignView.coffee b/app/views/play/CampaignView.coffee
index 641f19e1e..20062873a 100644
--- a/app/views/play/CampaignView.coffee
+++ b/app/views/play/CampaignView.coffee
@@ -397,7 +397,8 @@ module.exports = class CampaignView extends RootView
@particleMan.removeEmitters()
@particleMan.attach @$el.find('.map')
for level in @campaign.renderedLevels ? {}
- particleKey = ['level', @terrain.replace('-branching-test', '')]
+ terrain = @terrain.replace('-branching-test', '').replace(/(game|web)-dev-\d/, 'forest')
+ particleKey = ['level', terrain]
particleKey.push level.type if level.type and not (level.type in ['hero', 'course']) # Would use isType, but it's not a Level model
particleKey.push 'replayable' if level.replayable
particleKey.push 'premium' if level.requiresSubscription
diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee
index 7ba724ff9..a775a2d53 100644
--- a/app/views/play/level/PlayLevelView.coffee
+++ b/app/views/play/level/PlayLevelView.coffee
@@ -143,6 +143,7 @@ module.exports = class PlayLevelView extends RootView
onLevelLoaded: (e) ->
@god = new God({@gameUIState}) unless e.level.isType('web-dev')
+ @setUpGod() if @waitingToSetUpGod
trackLevelLoadEnd: ->
return if @isEditorPreview
@@ -242,6 +243,8 @@ module.exports = class PlayLevelView extends RootView
setupGod: ->
return if @level.isType('web-dev')
+ return @waitingToSetUpGod = true unless @god
+ @waitingToSetUpGod = undefined
@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
diff --git a/app/views/play/level/modal/HeroVictoryModal.coffee b/app/views/play/level/modal/HeroVictoryModal.coffee
index 2f55d546b..2b30a5660 100644
--- a/app/views/play/level/modal/HeroVictoryModal.coffee
+++ b/app/views/play/level/modal/HeroVictoryModal.coffee
@@ -75,7 +75,7 @@ module.exports = class HeroVictoryModal extends ModalView
@saveReviewEventually = _.debounce(@saveReviewEventually, 2000)
@loadExistingFeedback()
- if @level.isType('game-dev', 'web-dev')
+ if @level.get('shareable') is 'project'
@shareURL = "#{window.location.origin}/play/#{@level.get('type')}-level/#{@level.get('slug')}/#{@session.id}"
destroy: ->
diff --git a/app/views/play/level/modal/ProgressView.coffee b/app/views/play/level/modal/ProgressView.coffee
index 23d5879ef..0c35c61da 100644
--- a/app/views/play/level/modal/ProgressView.coffee
+++ b/app/views/play/level/modal/ProgressView.coffee
@@ -24,7 +24,7 @@ module.exports = class ProgressView extends CocoView
# Images in Markdown are like 
@nextLevel.get('description', true) # Make sure the defaults are available
@nextLevelDescription = marked(utils.i18n(@nextLevel.attributesWithDefaults, 'description').replace(/!\[.*?\]\(.*?\)\n*/g, ''))
- if @level.isType('game-dev', 'web-dev')
+ if @level.get('shareable') is 'project'
@shareURL = "#{window.location.origin}/play/#{@level.get('type')}-level/#{@level.get('slug')}/#{@session.id}"
@shareURL += "?course=#{@course.id}" if @course
From dc6a1de9fabf8e3d4679e848a37a8a66b7f203c0 Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Sat, 16 Jul 2016 00:31:53 -0700
Subject: [PATCH 44/58] Ordering/labeling courses: CS1, CS2, GD1, WD1, CS3, etc
---
app/templates/admin.jade | 2 +-
app/templates/courses/teacher-class-view.jade | 5 +++-
app/views/admin/MainAdminView.coffee | 22 ++++++++++++++-
app/views/courses/TeacherClassView.coffee | 19 +++++++++++--
server/models/Course.coffee | 28 +++++++++++++++++++
5 files changed, 70 insertions(+), 6 deletions(-)
diff --git a/app/templates/admin.jade b/app/templates/admin.jade
index cd323fc39..77dd88127 100644
--- a/app/templates/admin.jade
+++ b/app/templates/admin.jade
@@ -44,7 +44,7 @@ block content
a(href="/admin/classroom-levels") Classroom Levels
li
button.classroom-progress-csv.btn.btn-sm.btn-success Classroom Progress CSV
- input.classroom-progress-class-code(type=text value="")
+ input.classroom-progress-class-code(type=text placeholder="")
li
a(href="/admin/analytics") Dashboard
li
diff --git a/app/templates/courses/teacher-class-view.jade b/app/templates/courses/teacher-class-view.jade
index 283e8dfbe..5085fa80d 100644
--- a/app/templates/courses/teacher-class-view.jade
+++ b/app/templates/courses/teacher-class-view.jade
@@ -229,6 +229,8 @@ mixin studentRow(student)
+longLevelName(student.latestCompleteLevel)
td
if state.get('progressData')
+ - var courses = view.classroom.get('courses').map(function(c) { return view.courses.get(c._id); });
+ - var courseLabelsArray = view.courseLabelsArray(courses);
each trimCourse, index in view.classroom.get('courses')
- var course = view.courses.get(trimCourse._id);
- var instance = view.courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id })
@@ -236,7 +238,8 @@ mixin studentRow(student)
- var progress = state.get('progressData').get({ classroom: view.classroom, course: course, user: student })
- var levelsTotal = trimCourse.levels.length
//- - var level = ???
- +studentCourseProgressDot(progress, levelsTotal, level, 'CS' + (index+1))
+ - var label = courseLabelsArray[index];
+ +studentCourseProgressDot(progress, levelsTotal, level, label)
unless student.isEnrolled()
+enrollStudentButton(student)
//- td
diff --git a/app/views/admin/MainAdminView.coffee b/app/views/admin/MainAdminView.coffee
index dd4b33b32..7ceeda136 100644
--- a/app/views/admin/MainAdminView.coffee
+++ b/app/views/admin/MainAdminView.coffee
@@ -9,6 +9,7 @@ Campaigns = require 'collections/Campaigns'
Classroom = require 'models/Classroom'
CocoCollection = require 'collections/CocoCollection'
Course = require 'models/Course'
+Courses = require 'collections/Courses'
LevelSessions = require 'collections/LevelSessions'
User = require 'models/User'
Users = require 'collections/Users'
@@ -152,6 +153,7 @@ module.exports = class MainAdminView extends RootView
$('.classroom-progress-csv').prop('disabled', true)
classCode = $('.classroom-progress-class-code').val()
classroom = null
+ courses = null
courseLevels = []
sessions = null
users = null
@@ -161,12 +163,16 @@ module.exports = class MainAdminView extends RootView
classroom = new Classroom({ _id: model.data._id })
Promise.resolve(classroom.fetch())
.then (model) =>
+ courses = new Courses()
+ Promise.resolve(courses.fetch())
+ .then (models) =>
for course, index in classroom.get('courses')
for level in course.levels
courseLevels.push
courseIndex: index + 1
levelID: level.original
slug: level.slug
+ courseSlug: courses.get(course._id).get('slug')
users = new Users()
Promise.resolve($.when(users.fetchForClassroom(classroom)...))
.then (models) =>
@@ -202,12 +208,19 @@ module.exports = class MainAdminView extends RootView
columnLabels = "Username"
currentLevel = 1
+ courseLabelIndexes = CS: 1, GD: 0, WD: 0
lastCourseIndex = 1
+ lastCourseLabel = 'CS1'
for level in courseLevels
unless level.courseIndex is lastCourseIndex
currentLevel = 1
lastCourseIndex = level.courseIndex
- columnLabels += ",CS#{level.courseIndex}.#{currentLevel++} #{level.slug}"
+ acronym = switch
+ when /game-dev/.test(level.courseSlug) then 'GD'
+ when /web-dev/.test(level.courseSlug) then 'WD'
+ else 'CS'
+ lastCourseLabel = acronym + ++courseLabelIndexes[acronym]
+ columnLabels += ",#{lastCourseLabel}.#{currentLevel++} #{level.slug}"
csvContent = "data:text/csv;charset=utf-8,#{columnLabels}\n"
for studentRow in userPlaytimes
csvContent += studentRow.join(',') + "\n"
@@ -220,3 +233,10 @@ module.exports = class MainAdminView extends RootView
$('.classroom-progress-csv').prop('disabled', false)
console.error error
throw error
+
+ courseLabelsArray: (courses) ->
+ labels = []
+ courseLabelIndexes = CS: 0, GD: 0, WD: 0
+ for course in courses
+ labels.push acronym + ++courseLabelIndexes[acronym]
+ labels
diff --git a/app/views/courses/TeacherClassView.coffee b/app/views/courses/TeacherClassView.coffee
index 4aefcc800..059166a5c 100644
--- a/app/views/courses/TeacherClassView.coffee
+++ b/app/views/courses/TeacherClassView.coffee
@@ -321,9 +321,11 @@ module.exports = class TeacherClassView extends RootView
window.tracker?.trackEvent 'Teachers Class Export CSV', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
courseLabels = ""
courseOrder = []
- for course, index in @classroom.get('courses')
- courseLabels += "CS#{index + 1} Playtime,"
- courseOrder.push(course._id)
+ courses = (@courses.get(c._id) for c in @classroom.get('courses'))
+ courseLabelsArray = @courseLabelsArray courses
+ for course, index in courses
+ courseLabels += "#{courseLabelsArray[index]} Playtime,"
+ courseOrder.push(course.id)
csvContent = "data:text/csv;charset=utf-8,Username,Email,Total Playtime,#{courseLabels}Concepts\n"
levelCourseMap = {}
for trimCourse in @classroom.get('courses')
@@ -464,3 +466,14 @@ module.exports = class TeacherClassView extends RootView
when 'enrolled' then (if expires then $.i18n.t('teacher.status_enrolled') else '-')
when 'expired' then $.i18n.t('teacher.status_expired')
return string.replace('{{date}}', moment(expires).utc().format('l'))
+
+ courseLabelsArray: (courses) ->
+ labels = []
+ courseLabelIndexes = CS: 0, GD: 0, WD: 0
+ for course in courses
+ acronym = switch
+ when /game-dev/.test(course.get('slug')) then 'GD'
+ when /web-dev/.test(course.get('slug')) then 'WD'
+ else 'CS'
+ labels.push acronym + ++courseLabelIndexes[acronym]
+ labels
diff --git a/server/models/Course.coffee b/server/models/Course.coffee
index 7ab4ec71d..cdec72b8d 100644
--- a/server/models/Course.coffee
+++ b/server/models/Course.coffee
@@ -13,4 +13,32 @@ CourseSchema.statics.editableProperties = []
CourseSchema.statics.jsonSchema = jsonSchema
+CourseSchema.statics.sortCourses = (courses) ->
+ ordering = [
+ 'introduction-to-computer-science'
+ 'computer-science-2'
+ 'game-dev-1'
+ 'web-dev-1'
+ 'computer-science-3'
+ 'game-dev-1'
+ 'web-dev-2'
+ 'computer-science-4'
+ 'game-dev-3'
+ 'web-dev-3'
+ 'computer-science-5'
+ 'game-dev-4'
+ 'web-dev-4'
+ 'computer-science-6'
+ 'game-dev-5'
+ 'web-dev-5'
+ 'computer-science-7'
+ 'game-dev-6'
+ 'web-dev-6'
+ 'computer-science-8'
+ ]
+ _.sortBy courses, (course) ->
+ index = ordering.indexOf(course.get?('slug') or course.slug)
+ index = 9001 if index is -1
+ index
+
module.exports = Course = mongoose.model 'course', CourseSchema, 'courses'
From d37527d21be5e6a938dff578206623da9f1b6dc0 Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Sat, 16 Jul 2016 00:35:52 -0700
Subject: [PATCH 45/58] Ordering/labeling courses: CS1, CS2, GD1, WD1, CS3, etc
---
app/views/admin/MainAdminView.coffee | 7 -------
server/middleware/classrooms.coffee | 1 +
server/middleware/courses.coffee | 1 +
3 files changed, 2 insertions(+), 7 deletions(-)
diff --git a/app/views/admin/MainAdminView.coffee b/app/views/admin/MainAdminView.coffee
index 7ceeda136..f6be77f19 100644
--- a/app/views/admin/MainAdminView.coffee
+++ b/app/views/admin/MainAdminView.coffee
@@ -233,10 +233,3 @@ module.exports = class MainAdminView extends RootView
$('.classroom-progress-csv').prop('disabled', false)
console.error error
throw error
-
- courseLabelsArray: (courses) ->
- labels = []
- courseLabelIndexes = CS: 0, GD: 0, WD: 0
- for course in courses
- labels.push acronym + ++courseLabelIndexes[acronym]
- labels
diff --git a/server/middleware/classrooms.coffee b/server/middleware/classrooms.coffee
index 4ef0abfb8..e95af999a 100644
--- a/server/middleware/classrooms.coffee
+++ b/server/middleware/classrooms.coffee
@@ -145,6 +145,7 @@ module.exports =
query = {}
query = {adminOnly: {$ne: true}} unless req.user?.isAdmin()
courses = yield Course.find(query)
+ courses = Course.sortCourses courses
campaigns = yield Campaign.find({_id: {$in: (course.get('campaignID') for course in courses)}})
campaignMap = {}
campaignMap[campaign.id] = campaign for campaign in campaigns
diff --git a/server/middleware/courses.coffee b/server/middleware/courses.coffee
index 22cd0a48d..34b05715d 100644
--- a/server/middleware/courses.coffee
+++ b/server/middleware/courses.coffee
@@ -57,4 +57,5 @@ module.exports =
dbq = Model.find(query)
dbq.select(parse.getProjectFromReq(req))
results = yield database.viewSearch(dbq, req)
+ results = Course.sortCourses results
res.send(results)
From 0cd3278b8fbe5d346b0aeb8fa9d35f410b2a71a3 Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Sat, 16 Jul 2016 13:11:43 -0700
Subject: [PATCH 46/58] Add simple ImageGalleryView for some sample images in
web-dev levels
---
app/lib/coursesHelper.coffee | 1 +
.../play/modal/image-gallery-modal.sass | 11 +
.../play/level/modal/image-gallery-modal.jade | 23 +
.../play/level/tome/spell-top-bar-view.jade | 4 +
.../play/level/modal/ImageGalleryModal.coffee | 876 ++++++++++++++++++
.../play/level/tome/SpellTopBarView.coffee | 5 +
6 files changed, 920 insertions(+)
create mode 100644 app/styles/play/modal/image-gallery-modal.sass
create mode 100644 app/templates/play/level/modal/image-gallery-modal.jade
create mode 100644 app/views/play/level/modal/ImageGalleryModal.coffee
diff --git a/app/lib/coursesHelper.coffee b/app/lib/coursesHelper.coffee
index ad68f1fee..c50873c62 100644
--- a/app/lib/coursesHelper.coffee
+++ b/app/lib/coursesHelper.coffee
@@ -195,6 +195,7 @@ module.exports =
_.assign(progressData, progressMixin)
return progressData
+
progressMixin =
get: (options={}) ->
{ classroom, course, level, user } = options
diff --git a/app/styles/play/modal/image-gallery-modal.sass b/app/styles/play/modal/image-gallery-modal.sass
new file mode 100644
index 000000000..caf5d0eef
--- /dev/null
+++ b/app/styles/play/modal/image-gallery-modal.sass
@@ -0,0 +1,11 @@
+@import "app/styles/mixins"
+
+#image-gallery-modal
+ .modal-dialog
+ width: 800px
+
+ li
+ font-size: 12px
+
+ .no-select
+ @include user-select(none)
diff --git a/app/templates/play/level/modal/image-gallery-modal.jade b/app/templates/play/level/modal/image-gallery-modal.jade
new file mode 100644
index 000000000..b21862dc6
--- /dev/null
+++ b/app/templates/play/level/modal/image-gallery-modal.jade
@@ -0,0 +1,23 @@
+extends /templates/core/modal-base-flat
+
+block modal-header-content
+ h3(data-i18n='') Image Gallery
+ | Copy these images into your webpage, or find your own image URLs online.
+
+block modal-body-content
+ dl.dl-horizontal
+ for image in view.images
+ dt
+ img(src=image.portraitURL)
+ dd
+ ul.list-unstyled
+ li
+ span.no-select= 'URL: '
+ kbd= image.portraitURL
+ br
+ li
+ span.no-select= '
: '
+ kbd= '
'
+
+block modal-footer-content
+ a(href='#', data-dismiss="modal", aria-hidden="true", data-i18n="modal.close").btn.btn-primary Close
diff --git a/app/templates/play/level/tome/spell-top-bar-view.jade b/app/templates/play/level/tome/spell-top-bar-view.jade
index 30f0bf1de..f6a8faa2c 100644
--- a/app/templates/play/level/tome/spell-top-bar-view.jade
+++ b/app/templates/play/level/tome/spell-top-bar-view.jade
@@ -21,6 +21,10 @@
.btn.btn-small.btn-illustrated.hints-button
span(data-i18n="play_level.hints")
+ if view.options.level.isType('web-dev')
+ .btn.btn-small.btn-illustrated.image-gallery-button
+ span(data-i18n='') Image Gallery
+
if view.options.level.get('shareable')
- var url = '/play/' + view.options.level.get('type') + '-level/' + view.options.level.get('slug') + '/' + view.options.session.id;
- if (view.options.courseID) url += '?course=' + view.options.courseID;
diff --git a/app/views/play/level/modal/ImageGalleryModal.coffee b/app/views/play/level/modal/ImageGalleryModal.coffee
new file mode 100644
index 000000000..50a300b59
--- /dev/null
+++ b/app/views/play/level/modal/ImageGalleryModal.coffee
@@ -0,0 +1,876 @@
+ModalView = require 'views/core/ModalView'
+
+module.exports = class ImageGalleryModal extends ModalView
+ id: 'image-gallery-modal'
+ template: require 'templates/play/level/modal/image-gallery-modal'
+ # Top most useful Thang portraits
+ images: [
+ {slug: 'archer-f', name: 'Archer F', original: '529ab1a24b67a988ad000002', portraitURL: '/file/db/thang.type/529ab1a24b67a988ad000002/portrait.png', kind: 'Unit'}
+ {slug: 'archer-m', name: 'Archer M', original: '52cee45a76ebd5196b00003a', portraitURL: '/file/db/thang.type/52cee45a76ebd5196b00003a/portrait.png', kind: 'Unit'}
+ {slug: 'artist', name: 'Artist', original: '56d0c4f601476e2100de76c0', portraitURL: '/file/db/thang.type/56d0c4f601476e2100de76c0/portrait.png', kind: 'Unit'}
+ {slug: 'assassin', name: 'Assassin', original: '566a2202e132c81f00f38c81', portraitURL: '/file/db/thang.type/566a2202e132c81f00f38c81/portrait.png', kind: 'Hero'}
+ {slug: 'baby-griffin', name: 'baby griffin', original: '57586f0a22179b2800efda37', portraitURL: '/file/db/thang.type/57586f0a22179b2800efda37/portrait.png', kind: 'Item'}
+ {slug: 'basic-flags', name: 'Basic Flags', original: '545bacb41e649a4495f887da', portraitURL: '/file/db/thang.type/545bacb41e649a4495f887da/portrait.png', kind: 'Item'}
+ {slug: 'boom-ball', name: 'Boom Ball', original: '54eb540b49fa2d5c905ddf1a', portraitURL: '/file/db/thang.type/54eb540b49fa2d5c905ddf1a/portrait.png', kind: 'Item'}
+ {slug: 'breaker', name: 'Breaker', original: '56d0dd5b441ddd2f002ba3d8', portraitURL: '/file/db/thang.type/56d0dd5b441ddd2f002ba3d8/portrait.png', kind: 'Unit'}
+ {slug: 'burl', name: 'Burl', original: '530e5926c06854403ba68693', portraitURL: '/file/db/thang.type/530e5926c06854403ba68693/portrait.png', kind: 'Unit'}
+ {slug: 'captain', name: 'Captain', original: '529ec584c423d4e83b000014', portraitURL: '/file/db/thang.type/529ec584c423d4e83b000014/portrait.png', kind: 'Hero'}
+ {slug: 'champion', name: 'Champion', original: '575848b522179b2800efbfbf', portraitURL: '/file/db/thang.type/575848b522179b2800efbfbf/portrait.png', kind: 'Hero'}
+ {slug: 'chest-of-gems', name: 'Chest of Gems', original: '5432f9d18364d30000d1f943', portraitURL: '/file/db/thang.type/5432f9d18364d30000d1f943/portrait.png', kind: 'Misc'}
+ {slug: 'confuse', name: 'Confuse', original: '53024b76a6efdd32359c5340', portraitURL: '/file/db/thang.type/53024b76a6efdd32359c5340/portrait.png', kind: 'Mark'}
+ {slug: 'control', name: 'Control', original: '53024c7b27471514685d5397', portraitURL: '/file/db/thang.type/53024c7b27471514685d5397/portrait.png', kind: 'Mark'}
+ {slug: 'cougar', name: 'Cougar', original: '5744e3683af6bf590cd27371', portraitURL: '/file/db/thang.type/5744e3683af6bf590cd27371/portrait.png', kind: 'Item'}
+ {slug: 'cow', name: 'Cow', original: '52e95a5022efc8e709001743', portraitURL: '/file/db/thang.type/52e95a5022efc8e709001743/portrait.png', kind: 'Doodad'}
+ {slug: 'dantdm', name: 'DanTDM', original: '578674c3a6c641350091b645', portraitURL: '/file/db/thang.type/578674c3a6c641350091b645/portrait.png', kind: 'Unit'}
+ {slug: 'desert-bones-2', name: 'Desert Bones 2', original: '548cf11b0f559d0000be7e2b', portraitURL: '/file/db/thang.type/548cf11b0f559d0000be7e2b/portrait.png', kind: 'Doodad'}
+ {slug: 'duelist', name: 'Duelist', original: '57588f09046caf2e0012ed41', portraitURL: '/file/db/thang.type/57588f09046caf2e0012ed41/portrait.png', kind: 'Hero'}
+ {slug: 'equestrian', name: 'Equestrian', original: '52e95b4222efc8e70900175d', portraitURL: '/file/db/thang.type/52e95b4222efc8e70900175d/portrait.png', kind: 'Unit'}
+ {slug: 'flower-1', name: 'Flower 1', original: '54e951c8f54ef5794f354ed1', portraitURL: '/file/db/thang.type/54e951c8f54ef5794f354ed1/portrait.png', kind: 'Doodad'}
+ {slug: 'flower-2', name: 'Flower 2', original: '54e9525ff54ef5794f354ed5', portraitURL: '/file/db/thang.type/54e9525ff54ef5794f354ed5/portrait.png', kind: 'Doodad'}
+ {slug: 'flower-3', name: 'Flower 3', original: '54e95293f54ef5794f354ed9', portraitURL: '/file/db/thang.type/54e95293f54ef5794f354ed9/portrait.png', kind: 'Doodad'}
+ {slug: 'flower-4', name: 'Flower 4', original: '54e952b7f54ef5794f354edd', portraitURL: '/file/db/thang.type/54e952b7f54ef5794f354edd/portrait.png', kind: 'Doodad'}
+ {slug: 'flower-5', name: 'Flower 5', original: '54e952daf54ef5794f354ee1', portraitURL: '/file/db/thang.type/54e952daf54ef5794f354ee1/portrait.png', kind: 'Doodad'}
+ {slug: 'flower-6', name: 'Flower 6', original: '54e95308f54ef5794f354ee5', portraitURL: '/file/db/thang.type/54e95308f54ef5794f354ee5/portrait.png', kind: 'Doodad'}
+ {slug: 'flower-7', name: 'Flower 7', original: '54e9532ff54ef5794f354ee9', portraitURL: '/file/db/thang.type/54e9532ff54ef5794f354ee9/portrait.png', kind: 'Doodad'}
+ {slug: 'flower-8', name: 'Flower 8', original: '54e9534ef54ef5794f354eed', portraitURL: '/file/db/thang.type/54e9534ef54ef5794f354eed/portrait.png', kind: 'Doodad'}
+ {slug: 'forest-archer', name: 'Forest Archer', original: '5466d4f2417c8b48a9811e87', portraitURL: '/file/db/thang.type/5466d4f2417c8b48a9811e87/portrait.png', kind: 'Hero'}
+ {slug: 'frozen-munchkin', name: 'Frozen Munchkin', original: '5576686e1e82182d9e6889bb', portraitURL: '/file/db/thang.type/5576686e1e82182d9e6889bb/portrait.png', kind: 'Doodad'}
+ {slug: 'frozen-soldier-f', name: 'Frozen Soldier F', original: '5576683e1e82182d9e6889b7', portraitURL: '/file/db/thang.type/5576683e1e82182d9e6889b7/portrait.png', kind: 'Doodad'}
+ {slug: 'frozen-soldier-m', name: 'Frozen Soldier M', original: '557662bf1e82182d9e6889af', portraitURL: '/file/db/thang.type/557662bf1e82182d9e6889af/portrait.png', kind: 'Doodad'}
+ {slug: 'gem', name: 'Gem', original: '52aa3b9eccbd588d4d000003', portraitURL: '/file/db/thang.type/52aa3b9eccbd588d4d000003/portrait.png', kind: 'Misc'}
+ {slug: 'gold-coin', name: 'Gold Coin', original: '535ef031c519160709f2f63a', portraitURL: '/file/db/thang.type/535ef031c519160709f2f63a/portrait.png', kind: 'Misc'}
+ {slug: 'goliath', name: 'Goliath', original: '55e1a6e876cb0948c96af9f8', portraitURL: '/file/db/thang.type/55e1a6e876cb0948c96af9f8/portrait.png', kind: 'Hero'}
+ {slug: 'guardian', name: 'Guardian', original: '566a058620de41290036a745', portraitURL: '/file/db/thang.type/566a058620de41290036a745/portrait.png', kind: 'Hero'}
+ {slug: 'horse', name: 'Horse', original: '52e989a4427172ae56001f04', portraitURL: '/file/db/thang.type/52e989a4427172ae56001f04/portrait.png', kind: 'Doodad'}
+ {slug: 'knight', name: 'Knight', original: '529ffbf1cf1818f2be000001', portraitURL: '/file/db/thang.type/529ffbf1cf1818f2be000001/portrait.png', kind: 'Hero'}
+ {slug: 'librarian', name: 'Librarian', original: '52fbf74b7e01835453bd8d8e', portraitURL: '/file/db/thang.type/52fbf74b7e01835453bd8d8e/portrait.png', kind: 'Hero'}
+ {slug: 'necromancer', name: 'Necromancer', original: '55652fb3b9effa46a1f775fd', portraitURL: '/file/db/thang.type/55652fb3b9effa46a1f775fd/portrait.png', kind: 'Hero'}
+ {slug: 'ninja', name: 'Ninja', original: '52fc0ed77e01835453bd8f6c', portraitURL: '/file/db/thang.type/52fc0ed77e01835453bd8f6c/portrait.png', kind: 'Hero'}
+ {slug: 'ogre-brawler', name: 'Ogre Brawler', original: '529e5ee76febb9ca7e00000b', portraitURL: '/file/db/thang.type/529e5ee76febb9ca7e00000b/portrait.png', kind: 'Unit'}
+ {slug: 'ogre-chieftain', name: 'Ogre Chieftain', original: '55370661428ddac5686fd026', portraitURL: '/file/db/thang.type/55370661428ddac5686fd026/portrait.png', kind: 'Unit'}
+ {slug: 'ogre-f', name: 'Ogre F', original: '52cedd3e0b0d5c1b4c003ec6', portraitURL: '/file/db/thang.type/52cedd3e0b0d5c1b4c003ec6/portrait.png', kind: 'Unit'}
+ {slug: 'ogre-fangrider', name: 'Ogre Fangrider', original: '529e5f0c6febb9ca7e00000c', portraitURL: '/file/db/thang.type/529e5f0c6febb9ca7e00000c/portrait.png', kind: 'Unit'}
+ {slug: 'ogre-headhunter', name: 'Ogre Headhunter', original: '54c96c3cdef3ad363ff998a1', portraitURL: '/file/db/thang.type/54c96c3cdef3ad363ff998a1/portrait.png', kind: 'Unit'}
+ {slug: 'ogre-m', name: 'Ogre M', original: '529e40856febb9ca7e000004', portraitURL: '/file/db/thang.type/529e40856febb9ca7e000004/portrait.png', kind: 'Unit'}
+ {slug: 'ogre-munchkin-f', name: 'Ogre Munchkin F', original: '52cee1d976ebd5196b000038', portraitURL: '/file/db/thang.type/52cee1d976ebd5196b000038/portrait.png', kind: 'Unit'}
+ {slug: 'ogre-munchkin-m', name: 'Ogre Munchkin M', original: '529e5d756febb9ca7e00000a', portraitURL: '/file/db/thang.type/529e5d756febb9ca7e00000a/portrait.png', kind: 'Unit'}
+ {slug: 'ogre-shaman', name: 'Ogre Shaman', original: '529f92f9dacd325127000008', portraitURL: '/file/db/thang.type/529f92f9dacd325127000008/portrait.png', kind: 'Unit'}
+ {slug: 'ogre-thrower', name: 'Ogre Thrower', original: '529fff23cf1818f2be000003', portraitURL: '/file/db/thang.type/529fff23cf1818f2be000003/portrait.png', kind: 'Unit'}
+ {slug: 'ogre-warlock', name: 'Ogre Warlock', original: '5536f88c428ddac5686fd00c', portraitURL: '/file/db/thang.type/5536f88c428ddac5686fd00c/portrait.png', kind: 'Unit'}
+ {slug: 'ogre-witch', name: 'Ogre Witch', original: '5536ce98428ddac5686fcfd3', portraitURL: '/file/db/thang.type/5536ce98428ddac5686fcfd3/portrait.png', kind: 'Unit'}
+ {slug: 'oracle', name: 'Oracle', original: '56d0cfa063103d2a00af5449', portraitURL: '/file/db/thang.type/56d0cfa063103d2a00af5449/portrait.png', kind: 'Unit'}
+ {slug: 'paladin', name: 'Paladin', original: '552be965c54551e79b57b766', portraitURL: '/file/db/thang.type/552be965c54551e79b57b766/portrait.png', kind: 'Unit'}
+ {slug: 'peasant-f', name: 'Peasant F', original: '52d48f02d0ce9936e2000005', portraitURL: '/file/db/thang.type/52d48f02d0ce9936e2000005/portrait.png', kind: 'Unit'}
+ {slug: 'peasant-m', name: 'Peasant M', original: '529f9026dacd325127000005', portraitURL: '/file/db/thang.type/529f9026dacd325127000005/portrait.png', kind: 'Unit'}
+ {slug: 'polar-bear-cub', name: 'Polar Bear Cub', original: '578691f9bd31c1440083251d', portraitURL: '/file/db/thang.type/578691f9bd31c1440083251d/portrait.png', kind: 'Item'}
+ {slug: 'potion-master', name: 'Potion Master', original: '52e9adf7427172ae56002172', portraitURL: '/file/db/thang.type/52e9adf7427172ae56002172/portrait.png', kind: 'Hero'}
+ {slug: 'pugicorn', name: 'Pugicorn', original: '577d5d4dab818b210046b3bf', portraitURL: '/file/db/thang.type/577d5d4dab818b210046b3bf/portrait.png', kind: 'Item'}
+ {slug: 'raider', name: 'Raider', original: '55527eb0b8abf4ba1fe9a107', portraitURL: '/file/db/thang.type/55527eb0b8abf4ba1fe9a107/portrait.png', kind: 'Hero'}
+ {slug: 'raven', name: 'Raven', original: '5786a472a6c64135009238d3', portraitURL: '/file/db/thang.type/5786a472a6c64135009238d3/portrait.png', kind: 'Item'}
+ {slug: 'raven-pet', name: 'Raven Pet', original: '540f389a821af8000097dc5a', portraitURL: '/file/db/thang.type/540f389a821af8000097dc5a/portrait.png', kind: 'Unit'}
+ {slug: 'razordisc', name: 'Razordisc', original: '54eb4d5949fa2d5c905ddf06', portraitURL: '/file/db/thang.type/54eb4d5949fa2d5c905ddf06/portrait.png', kind: 'Item'}
+ {slug: 'samurai', name: 'Samurai', original: '53e12be0d042f23505c3023b', portraitURL: '/file/db/thang.type/53e12be0d042f23505c3023b/portrait.png', kind: 'Hero'}
+ {slug: 'skeleton', name: 'Skeleton', original: '54c83b8ae2829db30d0310e0', portraitURL: '/file/db/thang.type/54c83b8ae2829db30d0310e0/portrait.png', kind: 'Unit'}
+ {slug: 'soldier-f', name: 'Soldier F', original: '52d49552d0ce9936e2000007', portraitURL: '/file/db/thang.type/52d49552d0ce9936e2000007/portrait.png', kind: 'Unit'}
+ {slug: 'soldier-m', name: 'Soldier M', original: '529e680ac423d4e83b000001', portraitURL: '/file/db/thang.type/529e680ac423d4e83b000001/portrait.png', kind: 'Unit'}
+ {slug: 'sorcerer', name: 'Sorcerer', original: '52fd1524c7e6cf99160e7bc9', portraitURL: '/file/db/thang.type/52fd1524c7e6cf99160e7bc9/portrait.png', kind: 'Hero'}
+ {slug: 'target', name: 'Target', original: '52b32ad97385ec3d03000001', portraitURL: '/file/db/thang.type/52b32ad97385ec3d03000001/portrait.png', kind: 'Mark'}
+ {slug: 'thoktar', name: 'Thoktar', original: '52a00542cf1818f2be000006', portraitURL: '/file/db/thang.type/52a00542cf1818f2be000006/portrait.png', kind: 'Unit'}
+ {slug: 'tinker', name: 'Tinker', original: '56cdd89be906e72400f13451', portraitURL: '/file/db/thang.type/56cdd89be906e72400f13451/portrait.png', kind: 'Unit'}
+ {slug: 'trapper', name: 'Trapper', original: '5466d449417c8b48a9811e83', portraitURL: '/file/db/thang.type/5466d449417c8b48a9811e83/portrait.png', kind: 'Hero'}
+ {slug: 'wizard', name: 'Wizard', original: '52a00d55cf1818f2be00000b', portraitURL: '/file/db/thang.type/52a00d55cf1818f2be00000b/portrait.png', kind: 'Unit'}
+ {slug: 'wyrm', name: 'Wyrm', original: '56ba2b34e942de2600c792ed', portraitURL: '/file/db/thang.type/56ba2b34e942de2600c792ed/portrait.png', kind: 'Unit'}
+ ]
+
+ # Ones we didn't decide to use
+ otherImages: [
+ {slug: 'hero-placeholder', name: 'Hero Placeholder', original: '53ed1d9c2b65b0e32b9c96a9', portraitURL: '/file/db/thang.type/53ed1d9c2b65b0e32b9c96a9/portrait.png', kind: 'Unit'}
+ {slug: 'flag', name: 'Flag', original: '53fa25f25bc220000052c2be', portraitURL: '/file/db/thang.type/53fa25f25bc220000052c2be/portrait.png', kind: 'Misc'}
+ {slug: 'ace-of-coders-background', name: 'Ace of Coders Background', original: '55ef24a10e11a95a0d0ab103', portraitURL: '/file/db/thang.type/55ef24a10e11a95a0d0ab103/portrait.png', kind: 'Floor'}
+ {slug: 'advanced-flags', name: 'Advanced Flags', original: '5478b97e8707a2c3a2493b2f', portraitURL: '/file/db/thang.type/5478b97e8707a2c3a2493b2f/portrait.png', kind: 'Item'}
+ {slug: 'aerial-spear', name: 'Aerial Spear', original: '5400da521130f1881ca255e4', portraitURL: '/file/db/thang.type/5400da521130f1881ca255e4/portrait.png', kind: 'Misc'}
+ {slug: 'altar', name: 'Altar', original: '54ef8eb683b08b7d054b7f04', portraitURL: '/file/db/thang.type/54ef8eb683b08b7d054b7f04/portrait.png', kind: 'Doodad'}
+ {slug: 'amber-sense-stone', name: 'Amber Sense Stone', original: '54693413a2b1f53ce79443dd', portraitURL: '/file/db/thang.type/54693413a2b1f53ce79443dd/portrait.png', kind: 'Item'}
+ {slug: 'angel-fountain', name: 'Angel Fountain', original: '54f11438021968810565376b', portraitURL: '/file/db/thang.type/54f11438021968810565376b/portrait.png', kind: 'Doodad'}
+ {slug: 'angel-statue', name: 'Angel Statue', original: '54f1152a021968810565378a', portraitURL: '/file/db/thang.type/54f1152a021968810565378a/portrait.png', kind: 'Doodad'}
+ {slug: 'archway', name: 'Archway', original: '534dd3531a52ddd804f34efc', portraitURL: '/file/db/thang.type/534dd3531a52ddd804f34efc/portrait.png', kind: 'Misc'}
+ {slug: 'arrow', name: 'Arrow', original: '529ce66b0bf0bccdc6000005', portraitURL: '/file/db/thang.type/529ce66b0bf0bccdc6000005/portrait.png', kind: 'Missile'}
+ {slug: 'arrow-tower', name: 'Arrow Tower', original: '529f93cfdacd32512700000a', portraitURL: '/file/db/thang.type/529f93cfdacd32512700000a/portrait.png', kind: 'Unit'}
+ {slug: 'artillery', name: 'Artillery', original: '529e7a16c423d4e83b000003', portraitURL: '/file/db/thang.type/529e7a16c423d4e83b000003/portrait.png', kind: 'Unit'}
+ {slug: 'baby-griffin-pet', name: 'Baby Griffin Pet', original: '5750ef2f9f734c20005f1f57', portraitURL: '/file/db/thang.type/5750ef2f9f734c20005f1f57/portrait.png', kind: 'Unit'}
+ {slug: 'ball', name: 'Ball', original: '5580af39b43ce0b15a91b299', portraitURL: '/file/db/thang.type/5580af39b43ce0b15a91b299/portrait.png', kind: 'Doodad'}
+ {slug: 'balsa-staff', name: 'Balsa Staff', original: '544d88478494308424f56505', portraitURL: '/file/db/thang.type/544d88478494308424f56505/portrait.png', kind: 'Item'}
+ {slug: 'banded-redwood-wand', name: 'Banded Redwood Wand', original: '544d887c8494308424f56509', portraitURL: '/file/db/thang.type/544d887c8494308424f56509/portrait.png', kind: 'Item'}
+ {slug: 'barn', name: 'Barn', original: '54f1136f25be5e88058374b3', portraitURL: '/file/db/thang.type/54f1136f25be5e88058374b3/portrait.png', kind: 'Doodad'}
+ {slug: 'barrel', name: 'Barrel', original: '52aa5ff120fccb0000000003', portraitURL: '/file/db/thang.type/52aa5ff120fccb0000000003/portrait.png', kind: 'Doodad'}
+ {slug: 'barrel-animated', name: 'Barrel Animated', original: '54d2b28e7e1b915605556c37', portraitURL: '/file/db/thang.type/54d2b28e7e1b915605556c37/portrait.png', kind: 'Doodad'}
+ {slug: 'barrel-animated-2', name: 'Barrel Animated 2', original: '54d2b4fdae912a520569cff1', portraitURL: '/file/db/thang.type/54d2b4fdae912a520569cff1/portrait.png', kind: 'Doodad'}
+ {slug: 'bat', name: 'Bat', original: '55c13175c87e47c60604f987', portraitURL: '/file/db/thang.type/55c13175c87e47c60604f987/portrait.png', kind: 'Doodad'}
+ {slug: 'beam', name: 'Beam', original: '529ec2cec423d4e83b000011', portraitURL: '/file/db/thang.type/529ec2cec423d4e83b000011/portrait.png', kind: 'Missile'}
+ {slug: 'beam-tower', name: 'Beam Tower', original: '529ec0c1c423d4e83b00000d', portraitURL: '/file/db/thang.type/529ec0c1c423d4e83b00000d/portrait.png', kind: 'Unit'}
+ {slug: 'bear', name: 'Bear', original: '54e95b22f54ef5794f354f41', portraitURL: '/file/db/thang.type/54e95b22f54ef5794f354f41/portrait.png', kind: 'Doodad'}
+ {slug: 'bear-trap', name: 'Bear Trap', original: '54d2b8ef3e16915505f0bfeb', portraitURL: '/file/db/thang.type/54d2b8ef3e16915505f0bfeb/portrait.png', kind: 'Doodad'}
+ {slug: 'big-rocks-1', name: 'Big Rocks 1', original: '557f950db43ce0b15a91b1d9', portraitURL: '/file/db/thang.type/557f950db43ce0b15a91b1d9/portrait.png', kind: 'Doodad'}
+ {slug: 'big-rocks-2', name: 'Big Rocks 2', original: '557f959ab43ce0b15a91b1dd', portraitURL: '/file/db/thang.type/557f959ab43ce0b15a91b1dd/portrait.png', kind: 'Doodad'}
+ {slug: 'big-rocks-3', name: 'Big Rocks 3', original: '557f95e7b43ce0b15a91b1e1', portraitURL: '/file/db/thang.type/557f95e7b43ce0b15a91b1e1/portrait.png', kind: 'Doodad'}
+ {slug: 'big-rocks-4', name: 'Big Rocks 4', original: '557f9627b43ce0b15a91b1e5', portraitURL: '/file/db/thang.type/557f9627b43ce0b15a91b1e5/portrait.png', kind: 'Doodad'}
+ {slug: 'big-rocks-5', name: 'Big Rocks 5', original: '557f9661b43ce0b15a91b1e9', portraitURL: '/file/db/thang.type/557f9661b43ce0b15a91b1e9/portrait.png', kind: 'Doodad'}
+ {slug: 'bird', name: 'Bird', original: '53e2e31f6f406a3505b3eab0', portraitURL: '/file/db/thang.type/53e2e31f6f406a3505b3eab0/portrait.png', kind: 'Doodad'}
+ {slug: 'bloodhenge', name: 'Bloodhenge', original: '54f1168802196881056537df', portraitURL: '/file/db/thang.type/54f1168802196881056537df/portrait.png', kind: 'Doodad'}
+ {slug: 'blue-cart', name: 'Blue Cart', original: '5435d3207b554def1f99c49c', portraitURL: '/file/db/thang.type/5435d3207b554def1f99c49c/portrait.png', kind: 'Doodad'}
+ {slug: 'bluff-1', name: 'Bluff 1', original: '52afce51c5b1813ec200001a', portraitURL: '/file/db/thang.type/52afce51c5b1813ec200001a/portrait.png', kind: 'Doodad'}
+ {slug: 'bluff-2', name: 'Bluff 2', original: '52afcecbc5b1813ec200001c', portraitURL: '/file/db/thang.type/52afcecbc5b1813ec200001c/portrait.png', kind: 'Doodad'}
+ {slug: 'bolt', name: 'Bolt', original: '55c658a8a03e2014d693990a', portraitURL: '/file/db/thang.type/55c658a8a03e2014d693990a/portrait.png', kind: 'Missile'}
+ {slug: 'bolt-spitter', name: 'Bolt Spitter', original: '544d85d88494308424f564e4', portraitURL: '/file/db/thang.type/544d85d88494308424f564e4/portrait.png', kind: 'Item'}
+ {slug: 'boltsaw', name: 'Boltsaw', original: '544d6f5e8494308424f56476', portraitURL: '/file/db/thang.type/544d6f5e8494308424f56476/portrait.png', kind: 'Item'}
+ {slug: 'bone-dagger', name: 'Bone Dagger', original: '54eb4b2249fa2d5c905ddefe', portraitURL: '/file/db/thang.type/54eb4b2249fa2d5c905ddefe/portrait.png', kind: 'Item'}
+ {slug: 'book-of-life-i', name: 'Book of Life I', original: '546375653839c6e02811d30b', portraitURL: '/file/db/thang.type/546375653839c6e02811d30b/portrait.png', kind: 'Item'}
+ {slug: 'book-of-life-ii', name: 'Book of Life II', original: '546375813839c6e02811d30e', portraitURL: '/file/db/thang.type/546375813839c6e02811d30e/portrait.png', kind: 'Item'}
+ {slug: 'book-of-life-iii', name: 'Book of Life III', original: '546375a43839c6e02811d311', portraitURL: '/file/db/thang.type/546375a43839c6e02811d311/portrait.png', kind: 'Item'}
+ {slug: 'book-of-life-iv', name: 'Book of Life IV', original: '546376ca3839c6e02811d31d', portraitURL: '/file/db/thang.type/546376ca3839c6e02811d31d/portrait.png', kind: 'Item'}
+ {slug: 'book-of-life-v', name: 'Book of Life V', original: '546376ea3839c6e02811d320', portraitURL: '/file/db/thang.type/546376ea3839c6e02811d320/portrait.png', kind: 'Item'}
+ {slug: 'bookshelf', name: 'Bookshelf', original: '52e994ea427172ae56001fc9', portraitURL: '/file/db/thang.type/52e994ea427172ae56001fc9/portrait.png', kind: 'Doodad'}
+ {slug: 'bookshelf-2', name: 'Bookshelf 2', original: '54ef925a64112781056c18b5', portraitURL: '/file/db/thang.type/54ef925a64112781056c18b5/portrait.png', kind: 'Doodad'}
+ {slug: 'boom-ball-missile', name: 'Boom Ball Missile', original: '5535b5d4428ddac5686fcf82', portraitURL: '/file/db/thang.type/5535b5d4428ddac5686fcf82/portrait.png', kind: 'Missile'}
+ {slug: 'boomrod', name: 'Boomrod', original: '544d85898494308424f564df', portraitURL: '/file/db/thang.type/544d85898494308424f564df/portrait.png', kind: 'Item'}
+ {slug: 'boots-of-jumping', name: 'Boots of Jumping', original: '546d4e289df4a17d0d449ad5', portraitURL: '/file/db/thang.type/546d4e289df4a17d0d449ad5/portrait.png', kind: 'Item'}
+ {slug: 'boots-of-leaping', name: 'Boots of Leaping', original: '53e214f153457600003e3eab', portraitURL: '/file/db/thang.type/53e214f153457600003e3eab/portrait.png', kind: 'Item'}
+ {slug: 'boss-star-i', name: 'Boss Star I', original: '54eb58e449fa2d5c905ddf46', portraitURL: '/file/db/thang.type/54eb58e449fa2d5c905ddf46/portrait.png', kind: 'Item'}
+ {slug: 'boss-star-ii', name: 'Boss Star II', original: '54eb5bf649fa2d5c905ddf4a', portraitURL: '/file/db/thang.type/54eb5bf649fa2d5c905ddf4a/portrait.png', kind: 'Item'}
+ {slug: 'boss-star-iii', name: 'Boss Star III', original: '54eb5c8f49fa2d5c905ddf4e', portraitURL: '/file/db/thang.type/54eb5c8f49fa2d5c905ddf4e/portrait.png', kind: 'Item'}
+ {slug: 'boss-star-iv', name: 'Boss Star IV', original: '54eb5d1649fa2d5c905ddf52', portraitURL: '/file/db/thang.type/54eb5d1649fa2d5c905ddf52/portrait.png', kind: 'Item'}
+ {slug: 'boss-star-v', name: 'Boss Star V', original: '54eb5dbc49fa2d5c905ddf56', portraitURL: '/file/db/thang.type/54eb5dbc49fa2d5c905ddf56/portrait.png', kind: 'Item'}
+ {slug: 'boulder', name: 'Boulder', original: '544d86828494308424f564ec', portraitURL: '/file/db/thang.type/544d86828494308424f564ec/portrait.png', kind: 'Missile'}
+ {slug: 'boulder-trap', name: 'Boulder Trap', original: '55c246b1dfc8d0b576e60a23', portraitURL: '/file/db/thang.type/55c246b1dfc8d0b576e60a23/portrait.png', kind: 'Doodad'}
+ {slug: 'box', name: 'Box', original: '54d2b68a3e16915505f0bc8a', portraitURL: '/file/db/thang.type/54d2b68a3e16915505f0bc8a/portrait.png', kind: 'Doodad'}
+ {slug: 'box-2', name: 'Box 2', original: '54d2b797051a3a5305424c62', portraitURL: '/file/db/thang.type/54d2b797051a3a5305424c62/portrait.png', kind: 'Doodad'}
+ {slug: 'brawlwood', name: 'Brawlwood', original: '533b1f1642aef2202fdcc487', portraitURL: '/file/db/thang.type/533b1f1642aef2202fdcc487/portrait.png', kind: 'Floor'}
+ {slug: 'breakout-background', name: 'Breakout Background', original: '56c65f8b79735337006047df', portraitURL: '/file/db/thang.type/56c65f8b79735337006047df/portrait.png', kind: 'Floor'}
+ {slug: 'broken-tower', name: 'Broken Tower', original: '5376b2caff7b2d3805a396a9', portraitURL: '/file/db/thang.type/5376b2caff7b2d3805a396a9/portrait.png', kind: 'Doodad'}
+ {slug: 'bronze-coin', name: 'Bronze Coin', original: '535ef2d54f10444d08486ba8', portraitURL: '/file/db/thang.type/535ef2d54f10444d08486ba8/portrait.png', kind: 'Misc'}
+ {slug: 'bronze-shield', name: 'Bronze Shield', original: '544c310ae0017993fce214bf', portraitURL: '/file/db/thang.type/544c310ae0017993fce214bf/portrait.png', kind: 'Item'}
+ {slug: 'bullet', name: 'Bullet', original: '544d82bd8494308424f564d0', portraitURL: '/file/db/thang.type/544d82bd8494308424f564d0/portrait.png', kind: 'Missile'}
+ {slug: 'cabin-1', name: 'Cabin 1', original: '54e93b41970f0b0a263c0400', portraitURL: '/file/db/thang.type/54e93b41970f0b0a263c0400/portrait.png', kind: 'Doodad'}
+ {slug: 'cabin-2', name: 'Cabin 2', original: '54e93cb4970f0b0a263c0406', portraitURL: '/file/db/thang.type/54e93cb4970f0b0a263c0406/portrait.png', kind: 'Doodad'}
+ {slug: 'cabin-3', name: 'Cabin 3', original: '54e93d1cf54ef5794f354e7d', portraitURL: '/file/db/thang.type/54e93d1cf54ef5794f354e7d/portrait.png', kind: 'Doodad'}
+ {slug: 'cabin-4', name: 'Cabin 4', original: '54e93db7f54ef5794f354e83', portraitURL: '/file/db/thang.type/54e93db7f54ef5794f354e83/portrait.png', kind: 'Doodad'}
+ {slug: 'cabinet', name: 'Cabinet', original: '54ef9101c1f3bd7c0593f232', portraitURL: '/file/db/thang.type/54ef9101c1f3bd7c0593f232/portrait.png', kind: 'Doodad'}
+ {slug: 'cactus-1', name: 'Cactus 1', original: '546e24949df4a17d0d449bc5', portraitURL: '/file/db/thang.type/546e24949df4a17d0d449bc5/portrait.png', kind: 'Doodad'}
+ {slug: 'cactus-2', name: 'Cactus 2', original: '546e24039df4a17d0d449bb9', portraitURL: '/file/db/thang.type/546e24039df4a17d0d449bb9/portrait.png', kind: 'Doodad'}
+ {slug: 'caltrop-belt', name: 'Caltrop Belt', original: '54694af7a2b1f53ce7944441', portraitURL: '/file/db/thang.type/54694af7a2b1f53ce7944441/portrait.png', kind: 'Item'}
+ {slug: 'caltrops', name: 'Caltrops', original: '557f9700b43ce0b15a91b1ed', portraitURL: '/file/db/thang.type/557f9700b43ce0b15a91b1ed/portrait.png', kind: 'Doodad'}
+ {slug: 'camel', name: 'Camel', original: '548cf4cd0f559d0000be7e57', portraitURL: '/file/db/thang.type/548cf4cd0f559d0000be7e57/portrait.png', kind: 'Doodad'}
+ {slug: 'camp-fire', name: 'Camp Fire', original: '52e097c110012a5b250000b2', portraitURL: '/file/db/thang.type/52e097c110012a5b250000b2/portrait.png', kind: 'Doodad'}
+ {slug: 'campfire-stone', name: 'Campfire Stone', original: '54f118e125be5e880583759a', portraitURL: '/file/db/thang.type/54f118e125be5e880583759a/portrait.png', kind: 'Doodad'}
+ {slug: 'candle', name: 'Candle', original: '52e95fb222efc8e7090017d7', portraitURL: '/file/db/thang.type/52e95fb222efc8e7090017d7/portrait.png', kind: 'Doodad'}
+ {slug: 'carved-steel-ring', name: 'Carved Steel Ring', original: '54692dfaa2b1f53ce794439f', portraitURL: '/file/db/thang.type/54692dfaa2b1f53ce794439f/portrait.png', kind: 'Item'}
+ {slug: 'catapult', name: 'Catapult', original: '553e7ba29bdea5d00f1fd905', portraitURL: '/file/db/thang.type/553e7ba29bdea5d00f1fd905/portrait.png', kind: 'Unit'}
+ {slug: 'cave', name: 'Cave', original: '52e95983427172ae560018ce', portraitURL: '/file/db/thang.type/52e95983427172ae560018ce/portrait.png', kind: 'Doodad'}
+ {slug: 'chainmail-tunic', name: 'Chainmail Tunic', original: '5441c4dd4e9aeb727cc9713b', portraitURL: '/file/db/thang.type/5441c4dd4e9aeb727cc9713b/portrait.png', kind: 'Item'}
+ {slug: 'chains', name: 'Chains', original: '52aa602020fccb0000000004', portraitURL: '/file/db/thang.type/52aa602020fccb0000000004/portrait.png', kind: 'Doodad'}
+ {slug: 'chair', name: 'Chair', original: '52e9960e427172ae56001fdf', portraitURL: '/file/db/thang.type/52e9960e427172ae56001fdf/portrait.png', kind: 'Doodad'}
+ {slug: 'charge-belt', name: 'Charge Belt', original: '54694b27a2b1f53ce7944445', portraitURL: '/file/db/thang.type/54694b27a2b1f53ce7944445/portrait.png', kind: 'Item'}
+ {slug: 'choppable-tree-1', name: 'Choppable Tree 1', original: '52fbd1d67e01835453bd8a26', portraitURL: '/file/db/thang.type/52fbd1d67e01835453bd8a26/portrait.png', kind: 'Doodad'}
+ {slug: 'choppable-tree-2', name: 'Choppable Tree 2', original: '52fbd7e07e01835453bd8afc', portraitURL: '/file/db/thang.type/52fbd7e07e01835453bd8afc/portrait.png', kind: 'Doodad'}
+ {slug: 'choppable-tree-3', name: 'Choppable Tree 3', original: '52fbd9beab6e45c813bc79c6', portraitURL: '/file/db/thang.type/52fbd9beab6e45c813bc79c6/portrait.png', kind: 'Doodad'}
+ {slug: 'choppable-tree-4', name: 'Choppable Tree 4', original: '52fbdb747e01835453bd8b4a', portraitURL: '/file/db/thang.type/52fbdb747e01835453bd8b4a/portrait.png', kind: 'Doodad'}
+ {slug: 'circle-tree-stand-1', name: 'Circle Tree Stand 1', original: '541cb842c6362edfb0f3447d', portraitURL: '/file/db/thang.type/541cb842c6362edfb0f3447d/portrait.png', kind: 'Doodad'}
+ {slug: 'circle-tree-stand-2', name: 'Circle Tree Stand 2', original: '541cc5708e78524aad94de69', portraitURL: '/file/db/thang.type/541cc5708e78524aad94de69/portrait.png', kind: 'Doodad'}
+ {slug: 'circle-tree-stand-3', name: 'Circle Tree Stand 3', original: '541cc6898e78524aad94de6f', portraitURL: '/file/db/thang.type/541cc6898e78524aad94de6f/portrait.png', kind: 'Doodad'}
+ {slug: 'circlet-of-the-magi', name: 'Circlet of the Magi', original: '54ea39342b7506e891ca70f2', portraitURL: '/file/db/thang.type/54ea39342b7506e891ca70f2/portrait.png', kind: 'Item'}
+ {slug: 'classroom-bench', name: 'classroom bench', original: '56eb09520c6e9f1f00990e81', portraitURL: '/file/db/thang.type/56eb09520c6e9f1f00990e81/portrait.png', kind: 'Doodad'}
+ {slug: 'classroom-floor', name: 'Classroom Floor', original: '56a139f9d987c52900d4de5a', portraitURL: '/file/db/thang.type/56a139f9d987c52900d4de5a/portrait.png', kind: 'Floor'}
+ {slug: 'classroom-sculpture', name: 'Classroom Sculpture', original: '56a16510088f002400720564', portraitURL: '/file/db/thang.type/56a16510088f002400720564/portrait.png', kind: 'Doodad'}
+ {slug: 'classroom-students-desk', name: 'Classroom Students Desk', original: '56a15d88d987c52900d4ecdb', portraitURL: '/file/db/thang.type/56a15d88d987c52900d4ecdb/portrait.png', kind: 'Doodad'}
+ {slug: 'classroom-students-seat', name: 'Classroom Students Seat', original: '56a162348431922e0042fae3', portraitURL: '/file/db/thang.type/56a162348431922e0042fae3/portrait.png', kind: 'Doodad'}
+ {slug: 'classroom-viewscreen', name: 'Classroom Viewscreen', original: '569fdf3c6ff9591f000050bf', portraitURL: '/file/db/thang.type/569fdf3c6ff9591f000050bf/portrait.png', kind: 'Doodad'}
+ {slug: 'classroom-wall', name: 'Classroom Wall', original: '56a0150cf363ed1f0029e11c', portraitURL: '/file/db/thang.type/56a0150cf363ed1f0029e11c/portrait.png', kind: 'Wall'}
+ {slug: 'claymore', name: 'Claymore', original: '544d6d4a8494308424f56471', portraitURL: '/file/db/thang.type/544d6d4a8494308424f56471/portrait.png', kind: 'Item'}
+ {slug: 'cloud-1', name: 'Cloud 1', original: '550b42b7343675176d05a919', portraitURL: '/file/db/thang.type/550b42b7343675176d05a919/portrait.png', kind: 'Doodad'}
+ {slug: 'cloud-2', name: 'Cloud 2', original: '550b43fc343675176d05a923', portraitURL: '/file/db/thang.type/550b43fc343675176d05a923/portrait.png', kind: 'Doodad'}
+ {slug: 'cloud-3', name: 'Cloud 3', original: '550b4506343675176d05a933', portraitURL: '/file/db/thang.type/550b4506343675176d05a933/portrait.png', kind: 'Doodad'}
+ {slug: 'coin', name: 'Coin', original: '52aa3a8fccbd588d4d000001', portraitURL: '/file/db/thang.type/52aa3a8fccbd588d4d000001/portrait.png', kind: 'Misc'}
+ {slug: 'compound-boots', name: 'Compound Boots', original: '546d4d8e9df4a17d0d449acd', portraitURL: '/file/db/thang.type/546d4d8e9df4a17d0d449acd/portrait.png', kind: 'Item'}
+ {slug: 'cougar-pet', name: 'Cougar Pet', original: '540f3a33821af8000097dc62', portraitURL: '/file/db/thang.type/540f3a33821af8000097dc62/portrait.png', kind: 'Unit'}
+ {slug: 'crevasse-1', name: 'Crevasse 1', original: '5576080a1e82182d9e6888cd', portraitURL: '/file/db/thang.type/5576080a1e82182d9e6888cd/portrait.png', kind: 'Doodad'}
+ {slug: 'crevasse-2', name: 'Crevasse 2', original: '557630c31e82182d9e688921', portraitURL: '/file/db/thang.type/557630c31e82182d9e688921/portrait.png', kind: 'Doodad'}
+ {slug: 'crevasse-3', name: 'Crevasse 3', original: '557631321e82182d9e688925', portraitURL: '/file/db/thang.type/557631321e82182d9e688925/portrait.png', kind: 'Doodad'}
+ {slug: 'crisscross-back', name: 'Crisscross Back', original: '53b495e37e17883a05754216', portraitURL: '/file/db/thang.type/53b495e37e17883a05754216/portrait.png', kind: 'Floor'}
+ {slug: 'crisscross-front', name: 'Crisscross Front', original: '53b495b02082f23505b844e5', portraitURL: '/file/db/thang.type/53b495b02082f23505b844e5/portrait.png', kind: 'Floor'}
+ {slug: 'cross-bones-background', name: 'Cross Bones Background', original: '572e51175366918e018060e5', portraitURL: '/file/db/thang.type/572e51175366918e018060e5/portrait.png', kind: 'Floor'}
+ {slug: 'crossbeam-support', name: 'crossbeam support', original: '5786828a0d397a2e0026f274', portraitURL: '/file/db/thang.type/5786828a0d397a2e0026f274/portrait.png', kind: 'Doodad'}
+ {slug: 'crossbow', name: 'Crossbow', original: '53e21ae653457600003e3ec2', portraitURL: '/file/db/thang.type/53e21ae653457600003e3ec2/portrait.png', kind: 'Item'}
+ {slug: 'crude-builders-hammer', name: 'Crude Builder\'s Hammer', original: '53f4e6e3d822c23505b74f42', portraitURL: '/file/db/thang.type/53f4e6e3d822c23505b74f42/portrait.png', kind: 'Item'}
+ {slug: 'crude-crossbow', name: 'Crude Crossbow', original: '544d7ffd8494308424f564c3', portraitURL: '/file/db/thang.type/544d7ffd8494308424f564c3/portrait.png', kind: 'Item'}
+ {slug: 'crude-dagger', name: 'Crude Dagger', original: '544d952b8494308424f56517', portraitURL: '/file/db/thang.type/544d952b8494308424f56517/portrait.png', kind: 'Item'}
+ {slug: 'crude-dagger-missile', name: 'Crude Dagger Missile', original: '546e292d9df4a17d0d449c0c', portraitURL: '/file/db/thang.type/546e292d9df4a17d0d449c0c/portrait.png', kind: 'Missile'}
+ {slug: 'crude-glasses', name: 'Crude Glasses', original: '53e238df53457600003e3f0b', portraitURL: '/file/db/thang.type/53e238df53457600003e3f0b/portrait.png', kind: 'Item'}
+ {slug: 'crude-spike', name: 'Crude Spike', original: '544d79e28494308424f56482', portraitURL: '/file/db/thang.type/544d79e28494308424f56482/portrait.png', kind: 'Item'}
+ {slug: 'crude-telephoto-glasses', name: 'Crude Telephoto Glasses', original: '5469415aa2b1f53ce7944411', portraitURL: '/file/db/thang.type/5469415aa2b1f53ce7944411/portrait.png', kind: 'Item'}
+ {slug: 'crypt-key', name: 'Crypt Key', original: '54eb573549fa2d5c905ddf36', portraitURL: '/file/db/thang.type/54eb573549fa2d5c905ddf36/portrait.png', kind: 'Item'}
+ {slug: 'crystal-wand', name: 'Crystal Wand', original: '54eab63b2b7506e891ca71f2', portraitURL: '/file/db/thang.type/54eab63b2b7506e891ca71f2/portrait.png', kind: 'Item'}
+ {slug: 'cupboards-of-kgard-background', name: 'Cupboards of Kgard background', original: '56994ec3d32e4c1f0075460d', portraitURL: '/file/db/thang.type/56994ec3d32e4c1f0075460d/portrait.png', kind: 'Floor'}
+ {slug: 'curse', name: 'Curse', original: '53024d18a6efdd32359c5365', portraitURL: '/file/db/thang.type/53024d18a6efdd32359c5365/portrait.png', kind: 'Mark'}
+ {slug: 'cut-garnet-sense-stone', name: 'Cut Garnet Sense Stone', original: '546933a5a2b1f53ce79443d5', portraitURL: '/file/db/thang.type/546933a5a2b1f53ce79443d5/portrait.png', kind: 'Item'}
+ {slug: 'cut-stone-builders-hammer', name: 'Cut Stone Builder\'s Hammer', original: '54694c0ba2b1f53ce7944456', portraitURL: '/file/db/thang.type/54694c0ba2b1f53ce7944456/portrait.png', kind: 'Item'}
+ {slug: 'darksteel-blade', name: 'Darksteel Blade', original: '544d7f558494308424f564bb', portraitURL: '/file/db/thang.type/544d7f558494308424f564bb/portrait.png', kind: 'Item'}
+ {slug: 'deadeye-crossbow', name: 'Deadeye Crossbow', original: '54eaad752b7506e891ca71d1', portraitURL: '/file/db/thang.type/54eaad752b7506e891ca71d1/portrait.png', kind: 'Item'}
+ {slug: 'decoy', name: 'Decoy', original: '5498bb758e52573b10d3bce6', portraitURL: '/file/db/thang.type/5498bb758e52573b10d3bce6/portrait.png', kind: 'Unit'}
+ {slug: 'defensive-boots', name: 'Defensive Boots', original: '546d4e019df4a17d0d449ad1', portraitURL: '/file/db/thang.type/546d4e019df4a17d0d449ad1/portrait.png', kind: 'Item'}
+ {slug: 'defensive-infantry-shield', name: 'Defensive Infantry Shield', original: '544d7b408494308424f5648f', portraitURL: '/file/db/thang.type/544d7b408494308424f5648f/portrait.png', kind: 'Item'}
+ {slug: 'deflector', name: 'Deflector', original: '54eabff349fa2d5c905ddeee', portraitURL: '/file/db/thang.type/54eabff349fa2d5c905ddeee/portrait.png', kind: 'Item'}
+ {slug: 'derrick', name: 'Derrick', original: '546e24339df4a17d0d449bbd', portraitURL: '/file/db/thang.type/546e24339df4a17d0d449bbd/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-bones-1', name: 'Desert Bones 1', original: '548cf0cc0f559d0000be7e27', portraitURL: '/file/db/thang.type/548cf0cc0f559d0000be7e27/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-bones-3', name: 'Desert Bones 3', original: '548cf1630f559d0000be7e2f', portraitURL: '/file/db/thang.type/548cf1630f559d0000be7e2f/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-green-1', name: 'Desert Green 1', original: '548cef670f559d0000be7e17', portraitURL: '/file/db/thang.type/548cef670f559d0000be7e17/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-green-2', name: 'Desert Green 2', original: '548cefc50f559d0000be7e1b', portraitURL: '/file/db/thang.type/548cefc50f559d0000be7e1b/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-house-1', name: 'Desert House 1', original: '548cf35a0f559d0000be7e43', portraitURL: '/file/db/thang.type/548cf35a0f559d0000be7e43/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-house-2', name: 'Desert House 2', original: '548cf3ae0f559d0000be7e47', portraitURL: '/file/db/thang.type/548cf3ae0f559d0000be7e47/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-house-3', name: 'Desert House 3', original: '548cf4000f559d0000be7e4b', portraitURL: '/file/db/thang.type/548cf4000f559d0000be7e4b/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-house-4', name: 'Desert House 4', original: '548cf44c0f559d0000be7e4f', portraitURL: '/file/db/thang.type/548cf44c0f559d0000be7e4f/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-palm-1', name: 'Desert Palm 1', original: '548cf0110f559d0000be7e1f', portraitURL: '/file/db/thang.type/548cf0110f559d0000be7e1f/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-palm-2', name: 'Desert Palm 2', original: '548cf06f0f559d0000be7e23', portraitURL: '/file/db/thang.type/548cf06f0f559d0000be7e23/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-pillar', name: 'Desert Pillar', original: '541c5ff487338f570851ad83', portraitURL: '/file/db/thang.type/541c5ff487338f570851ad83/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-pyramid', name: 'Desert Pyramid', original: '53e239c253457600003e3f11', portraitURL: '/file/db/thang.type/53e239c253457600003e3f11/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-rubble-1', name: 'Desert Rubble 1', original: '53126c48f5a594b00fbfcc42', portraitURL: '/file/db/thang.type/53126c48f5a594b00fbfcc42/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-rubble-2', name: 'Desert Rubble 2', original: '52f01b0b5071878f7650e11a', portraitURL: '/file/db/thang.type/52f01b0b5071878f7650e11a/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-rubble-3', name: 'Desert Rubble 3', original: '546e23a89df4a17d0d449bb1', portraitURL: '/file/db/thang.type/546e23a89df4a17d0d449bb1/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-sand-rock', name: 'Desert Sand Rock', original: '55c64774ef141c65665beb84', portraitURL: '/file/db/thang.type/55c64774ef141c65665beb84/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-shrub-big-1', name: 'Desert Shrub Big 1', original: '546e237d9df4a17d0d449bad', portraitURL: '/file/db/thang.type/546e237d9df4a17d0d449bad/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-shrub-big-2', name: 'Desert Shrub Big 2', original: '546e22c59df4a17d0d449ba1', portraitURL: '/file/db/thang.type/546e22c59df4a17d0d449ba1/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-shrub-big-3', name: 'Desert Shrub Big 3', original: '53f4c776d822c23505b7091c', portraitURL: '/file/db/thang.type/53f4c776d822c23505b7091c/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-shrub-small-1', name: 'Desert Shrub Small 1', original: '548ceec80f559d0000be7e0f', portraitURL: '/file/db/thang.type/548ceec80f559d0000be7e0f/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-shrub-small-2', name: 'Desert Shrub Small 2', original: '548cef1f0f559d0000be7e13', portraitURL: '/file/db/thang.type/548cef1f0f559d0000be7e13/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-skullcave', name: 'Desert Skullcave', original: '546e231c9df4a17d0d449ba5', portraitURL: '/file/db/thang.type/546e231c9df4a17d0d449ba5/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-wall-1', name: 'Desert Wall 1', original: '5404fe5f1d10b2f170618ae9', portraitURL: '/file/db/thang.type/5404fe5f1d10b2f170618ae9/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-wall-2', name: 'Desert Wall 2', original: '540100ba794c1a8b4d328437', portraitURL: '/file/db/thang.type/540100ba794c1a8b4d328437/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-wall-3', name: 'Desert Wall 3', original: '53f4e7fff7bc7336054dcf64', portraitURL: '/file/db/thang.type/53f4e7fff7bc7336054dcf64/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-wall-4', name: 'Desert Wall 4', original: '53f3ef04e7a7643005c0f4a1', portraitURL: '/file/db/thang.type/53f3ef04e7a7643005c0f4a1/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-wall-5', name: 'Desert Wall 5', original: '53ebafdd1a100989a40ce479', portraitURL: '/file/db/thang.type/53ebafdd1a100989a40ce479/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-wall-6', name: 'Desert Wall 6', original: '53eb989b1a100989a40ce46a', portraitURL: '/file/db/thang.type/53eb989b1a100989a40ce46a/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-wall-7', name: 'Desert Wall 7', original: '53eaa7de786ccc3405a9f2a4', portraitURL: '/file/db/thang.type/53eaa7de786ccc3405a9f2a4/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-wall-8', name: 'Desert Wall 8', original: '53eaa6f6ef27b33605514a64', portraitURL: '/file/db/thang.type/53eaa6f6ef27b33605514a64/portrait.png', kind: 'Doodad'}
+ {slug: 'desert-well', name: 'Desert Well', original: '548cf4880f559d0000be7e53', portraitURL: '/file/db/thang.type/548cf4880f559d0000be7e53/portrait.png', kind: 'Doodad'}
+ {slug: 'destroyed-human-tower', name: 'destroyed human tower', original: '57867e5acca8994b002702a9', portraitURL: '/file/db/thang.type/57867e5acca8994b002702a9/portrait.png', kind: 'Doodad'}
+ {slug: 'destroyed-human-tower-with-trees', name: 'destroyed human tower with trees', original: '572d5abed7787fc300d85964', portraitURL: '/file/db/thang.type/572d5abed7787fc300d85964/portrait.png', kind: 'Doodad'}
+ {slug: 'destroyed-human-tower-with-trees-2', name: 'destroyed human tower with trees 2', original: '572d5b42d7787fc300d8596f', portraitURL: '/file/db/thang.type/572d5b42d7787fc300d8596f/portrait.png', kind: 'Doodad'}
+ {slug: 'destroyed-ogre-tower-footing', name: 'destroyed ogre tower footing', original: '578680980d397a2e0026eff9', portraitURL: '/file/db/thang.type/578680980d397a2e0026eff9/portrait.png', kind: 'Doodad'}
+ {slug: 'diamond-sense-stone', name: 'Diamond Sense Stone', original: '546934b7a2b1f53ce79443e1', portraitURL: '/file/db/thang.type/546934b7a2b1f53ce79443e1/portrait.png', kind: 'Item'}
+ {slug: 'dirt-path-1', name: 'Dirt Path 1', original: '5302acfd27471514685d5fd4', portraitURL: '/file/db/thang.type/5302acfd27471514685d5fd4/portrait.png', kind: 'Floor'}
+ {slug: 'disintegrate', name: 'Disintegrate', original: '54d2bb1abb157252059b1d29', portraitURL: '/file/db/thang.type/54d2bb1abb157252059b1d29/portrait.png', kind: 'Mark'}
+ {slug: 'dispel', name: 'Dispel', original: '55c2807d3767fd3435eb4465', portraitURL: '/file/db/thang.type/55c2807d3767fd3435eb4465/portrait.png', kind: 'Mark'}
+ {slug: 'dragonscale-chainmail-coif', name: 'Dragonscale Chainmail Coif', original: '546d477d9df4a17d0d449a6b', portraitURL: '/file/db/thang.type/546d477d9df4a17d0d449a6b/portrait.png', kind: 'Item'}
+ {slug: 'dragonscale-chainmail-tunic', name: 'Dragonscale Chainmail Tunic', original: '546d3d149df4a17d0d449a43', portraitURL: '/file/db/thang.type/546d3d149df4a17d0d449a43/portrait.png', kind: 'Item'}
+ {slug: 'dragontooth', name: 'Dragontooth', original: '54eb51d349fa2d5c905ddf0e', portraitURL: '/file/db/thang.type/54eb51d349fa2d5c905ddf0e/portrait.png', kind: 'Item'}
+ {slug: 'drain-life', name: 'Drain Life', original: '54d2bc5b4e4a08550556da55', portraitURL: '/file/db/thang.type/54d2bc5b4e4a08550556da55/portrait.png', kind: 'Mark'}
+ {slug: 'dread-door-background', name: 'Dread Door Background', original: '572e46a3f8c4f9b601ede6c0', portraitURL: '/file/db/thang.type/572e46a3f8c4f9b601ede6c0/portrait.png', kind: 'Floor'}
+ {slug: 'dueling-grounds-background', name: 'Dueling Grounds Background', original: '572e5163e8db5195014848b3', portraitURL: '/file/db/thang.type/572e5163e8db5195014848b3/portrait.png', kind: 'Floor'}
+ {slug: 'dunes', name: 'Dunes', original: '546e251d9df4a17d0d449bd1', portraitURL: '/file/db/thang.type/546e251d9df4a17d0d449bd1/portrait.png', kind: 'Doodad'}
+ {slug: 'dungeon-door', name: 'Dungeon Door', original: '52a0e5123abf480000000001', portraitURL: '/file/db/thang.type/52a0e5123abf480000000001/portrait.png', kind: 'Doodad'}
+ {slug: 'dungeon-entrance', name: 'Dungeon Entrance', original: '544d850e8494308424f564dd', portraitURL: '/file/db/thang.type/544d850e8494308424f564dd/portrait.png', kind: 'Doodad'}
+ {slug: 'dungeon-floor', name: 'Dungeon Floor', original: '52af688f6320a8049d000001', portraitURL: '/file/db/thang.type/52af688f6320a8049d000001/portrait.png', kind: 'Floor'}
+ {slug: 'dungeon-pillar', name: 'Dungeon Pillar', original: '543ea0ff9692aa00006208e7', portraitURL: '/file/db/thang.type/543ea0ff9692aa00006208e7/portrait.png', kind: 'Doodad'}
+ {slug: 'dungeon-pit', name: 'Dungeon Pit', original: '52b09408ccbc671372000002', portraitURL: '/file/db/thang.type/52b09408ccbc671372000002/portrait.png', kind: 'Floor'}
+ {slug: 'dungeon-rock-1', name: 'Dungeon Rock 1', original: '54ef944764112781056c1f96', portraitURL: '/file/db/thang.type/54ef944764112781056c1f96/portrait.png', kind: 'Doodad'}
+ {slug: 'dungeon-rock-2', name: 'Dungeon Rock 2', original: '54ef99bf223edd8105b00eaa', portraitURL: '/file/db/thang.type/54ef99bf223edd8105b00eaa/portrait.png', kind: 'Doodad'}
+ {slug: 'dungeon-rock-3', name: 'Dungeon Rock 3', original: '54ef9af5b4740779058448c6', portraitURL: '/file/db/thang.type/54ef9af5b4740779058448c6/portrait.png', kind: 'Doodad'}
+ {slug: 'dungeon-rock-4', name: 'Dungeon Rock 4', original: '54ef9c26933e1e7b0584663e', portraitURL: '/file/db/thang.type/54ef9c26933e1e7b0584663e/portrait.png', kind: 'Doodad'}
+ {slug: 'dungeon-rock-5', name: 'Dungeon Rock 5', original: '54ef9d376aea7d7805535cc8', portraitURL: '/file/db/thang.type/54ef9d376aea7d7805535cc8/portrait.png', kind: 'Doodad'}
+ {slug: 'dungeon-rock-group', name: 'Dungeon Rock Group', original: '54ef9e0583b08b7d054ba331', portraitURL: '/file/db/thang.type/54ef9e0583b08b7d054ba331/portrait.png', kind: 'Doodad'}
+ {slug: 'dungeon-stairs-horizontal', name: 'Dungeon Stairs Horizontal', original: '5463dc27c295cc4fb9c06257', portraitURL: '/file/db/thang.type/5463dc27c295cc4fb9c06257/portrait.png', kind: 'Doodad'}
+ {slug: 'dungeon-stairs-vertical', name: 'Dungeon Stairs Vertical', original: '5463d8a0c295cc4fb9c06255', portraitURL: '/file/db/thang.type/5463d8a0c295cc4fb9c06255/portrait.png', kind: 'Doodad'}
+ {slug: 'dungeon-wall', name: 'Dungeon Wall', original: '529e7aecc423d4e83b000004', portraitURL: '/file/db/thang.type/529e7aecc423d4e83b000004/portrait.png', kind: 'Wall'}
+ {slug: 'dungeons-of-kgard-background', name: 'Dungeons of Kgard Background', original: '563d3c02f5b71e8405fabff8', portraitURL: '/file/db/thang.type/563d3c02f5b71e8405fabff8/portrait.png', kind: 'Floor'}
+ {slug: 'dynamic-flags', name: 'Dynamic Flags', original: '5478b9068707a2c3a2493b2b', portraitURL: '/file/db/thang.type/5478b9068707a2c3a2493b2b/portrait.png', kind: 'Item'}
+ {slug: 'earthskin', name: 'Earthskin', original: '54d2bcf66ec7cf53051e7855', portraitURL: '/file/db/thang.type/54d2bcf66ec7cf53051e7855/portrait.png', kind: 'Mark'}
+ {slug: 'east-mounted-camera-facing-east-west', name: 'east mounted camera facing east west', original: '56f183091e1daf0a016c670b', portraitURL: '/file/db/thang.type/56f183091e1daf0a016c670b/portrait.png', kind: 'Doodad'}
+ {slug: 'east-mounted-camera-facing-north', name: 'east mounted camera facing north', original: '56f1782541c1a0cb00f8d66c', portraitURL: '/file/db/thang.type/56f1782541c1a0cb00f8d66c/portrait.png', kind: 'Doodad'}
+ {slug: 'east-mounted-camera-facing-south', name: 'east mounted camera facing south', original: '56f1811841c1a0cb00f8ddb1', portraitURL: '/file/db/thang.type/56f1811841c1a0cb00f8ddb1/portrait.png', kind: 'Doodad'}
+ {slug: 'edge-of-darkness', name: 'Edge of Darkness', original: '54eaa8762b7506e891ca71a9', portraitURL: '/file/db/thang.type/54eaa8762b7506e891ca71a9/portrait.png', kind: 'Item'}
+ {slug: 'eldritch-icicle', name: 'Eldritch Icicle', original: '54ea311e2b7506e891ca70b0', portraitURL: '/file/db/thang.type/54ea311e2b7506e891ca70b0/portrait.png', kind: 'Item'}
+ {slug: 'electrocute', name: 'Electrocute', original: '55c281263767fd3435eb4469', portraitURL: '/file/db/thang.type/55c281263767fd3435eb4469/portrait.png', kind: 'Mark'}
+ {slug: 'electrowall', name: 'Electrowall', original: '54177e26571f116c0b1f00c0', portraitURL: '/file/db/thang.type/54177e26571f116c0b1f00c0/portrait.png', kind: 'Doodad'}
+ {slug: 'elemental-codex-i', name: 'Elemental Codex I', original: '5463755a3839c6e02811d30a', portraitURL: '/file/db/thang.type/5463755a3839c6e02811d30a/portrait.png', kind: 'Item'}
+ {slug: 'elemental-codex-ii', name: 'Elemental Codex II', original: '546375783839c6e02811d30d', portraitURL: '/file/db/thang.type/546375783839c6e02811d30d/portrait.png', kind: 'Item'}
+ {slug: 'elemental-codex-iii', name: 'Elemental Codex III', original: '5463759c3839c6e02811d310', portraitURL: '/file/db/thang.type/5463759c3839c6e02811d310/portrait.png', kind: 'Item'}
+ {slug: 'elemental-codex-iv', name: 'Elemental Codex IV', original: '546376bf3839c6e02811d31c', portraitURL: '/file/db/thang.type/546376bf3839c6e02811d31c/portrait.png', kind: 'Item'}
+ {slug: 'elemental-codex-v', name: 'Elemental Codex V', original: '546376e23839c6e02811d31f', portraitURL: '/file/db/thang.type/546376e23839c6e02811d31f/portrait.png', kind: 'Item'}
+ {slug: 'embroidered-griffin-wool-hat', name: 'Embroidered Griffin Wool Hat', original: '546d4ca19df4a17d0d449abf', portraitURL: '/file/db/thang.type/546d4ca19df4a17d0d449abf/portrait.png', kind: 'Item'}
+ {slug: 'embroidered-griffin-wool-robe', name: 'Embroidered Griffin Wool Robe', original: '546d4a549df4a17d0d449a97', portraitURL: '/file/db/thang.type/546d4a549df4a17d0d449a97/portrait.png', kind: 'Item'}
+ {slug: 'emerald-chainmail-coif', name: 'Emerald Chainmail Coif', original: '546d46cf9df4a17d0d449a63', portraitURL: '/file/db/thang.type/546d46cf9df4a17d0d449a63/portrait.png', kind: 'Item'}
+ {slug: 'emerald-chainmail-tunic', name: 'Emerald Chainmail Tunic', original: '546d3c8d9df4a17d0d449a3b', portraitURL: '/file/db/thang.type/546d3c8d9df4a17d0d449a3b/portrait.png', kind: 'Item'}
+ {slug: 'emperors-gloves', name: 'Emperor\'s Gloves', original: '546949aca2b1f53ce7944431', portraitURL: '/file/db/thang.type/546949aca2b1f53ce7944431/portrait.png', kind: 'Item'}
+ {slug: 'enameled-dragonplate', name: 'Enameled Dragonplate', original: '546ab1e53777d61863292876', portraitURL: '/file/db/thang.type/546ab1e53777d61863292876/portrait.png', kind: 'Item'}
+ {slug: 'enameled-dragonplate-helmet', name: 'Enameled Dragonplate Helmet', original: '546d3a539df4a17d0d449a1f', portraitURL: '/file/db/thang.type/546d3a539df4a17d0d449a1f/portrait.png', kind: 'Item'}
+ {slug: 'enameled-dragonshield', name: 'Enameled Dragonshield', original: '54eabf022b7506e891ca7236', portraitURL: '/file/db/thang.type/54eabf022b7506e891ca7236/portrait.png', kind: 'Item'}
+ {slug: 'enchanted-lambswool-cloak', name: 'Enchanted Lambswool Cloak', original: '546d49109df4a17d0d449a7f', portraitURL: '/file/db/thang.type/546d49109df4a17d0d449a7f/portrait.png', kind: 'Item'}
+ {slug: 'enchanted-lenses', name: 'Enchanted Lenses', original: '546941cba2b1f53ce7944419', portraitURL: '/file/db/thang.type/546941cba2b1f53ce7944419/portrait.png', kind: 'Item'}
+ {slug: 'enchanted-stick', name: 'Enchanted Stick', original: '544d87188494308424f564f1', portraitURL: '/file/db/thang.type/544d87188494308424f564f1/portrait.png', kind: 'Item'}
+ {slug: 'enemy-mine-background', name: 'Enemy Mine Background', original: '563cdd340b2c7c87054e102b', portraitURL: '/file/db/thang.type/563cdd340b2c7c87054e102b/portrait.png', kind: 'Floor'}
+ {slug: 'energy-ball', name: 'Energy Ball', original: '53025d83222f73867774d8ed', portraitURL: '/file/db/thang.type/53025d83222f73867774d8ed/portrait.png', kind: 'Missile'}
+ {slug: 'energy-ball-diet', name: 'Energy Ball Diet', original: '531a6ddf1ddc910545d5e96d', portraitURL: '/file/db/thang.type/531a6ddf1ddc910545d5e96d/portrait.png', kind: 'Missile'}
+ {slug: 'enforcer', name: 'Enforcer', original: '56d0ca1263103d2a00af5331', portraitURL: '/file/db/thang.type/56d0ca1263103d2a00af5331/portrait.png', kind: 'Unit'}
+ {slug: 'engraved-builders-hammer', name: 'Engraved Builder\'s Hammer', original: '54694ca7a2b1f53ce7944462', portraitURL: '/file/db/thang.type/54694ca7a2b1f53ce7944462/portrait.png', kind: 'Item'}
+ {slug: 'engraved-obsidian-breastplate', name: 'Engraved Obsidian Breastplate', original: '546ab15e3777d6186329286e', portraitURL: '/file/db/thang.type/546ab15e3777d6186329286e/portrait.png', kind: 'Item'}
+ {slug: 'engraved-obsidian-helmet', name: 'Engraved Obsidian Helmet', original: '546d39d89df4a17d0d449a17', portraitURL: '/file/db/thang.type/546d39d89df4a17d0d449a17/portrait.png', kind: 'Item'}
+ {slug: 'engraved-obsidian-shield', name: 'Engraved Obsidian Shield', original: '54eabbd22b7506e891ca721e', portraitURL: '/file/db/thang.type/54eabbd22b7506e891ca721e/portrait.png', kind: 'Item'}
+ {slug: 'engraved-wristwatch', name: 'Engraved Wristwatch', original: '546937dea2b1f53ce79443ed', portraitURL: '/file/db/thang.type/546937dea2b1f53ce79443ed/portrait.png', kind: 'Item'}
+ {slug: 'explosive-potion', name: 'Explosive Potion', original: '5466d9a5417c8b48a9811e8e', portraitURL: '/file/db/thang.type/5466d9a5417c8b48a9811e8e/portrait.png', kind: 'Missile'}
+ {slug: 'ezeroths-timepiece', name: 'Ezeroth\'s Timepiece', original: '546938cea2b1f53ce79443f5', portraitURL: '/file/db/thang.type/546938cea2b1f53ce79443f5/portrait.png', kind: 'Item'}
+ {slug: 'farm', name: 'Farm', original: '52ea853d427172ae56003494', portraitURL: '/file/db/thang.type/52ea853d427172ae56003494/portrait.png', kind: 'Doodad'}
+ {slug: 'faux-fur-hat', name: 'Faux Fur Hat', original: '5441c2be4e9aeb727cc97105', portraitURL: '/file/db/thang.type/5441c2be4e9aeb727cc97105/portrait.png', kind: 'Item'}
+ {slug: 'fear', name: 'Fear', original: '53024db827471514685d53b2', portraitURL: '/file/db/thang.type/53024db827471514685d53b2/portrait.png', kind: 'Mark'}
+ {slug: 'fence', name: 'Fence', original: '5421bc5218adb78d98d265e8', portraitURL: '/file/db/thang.type/5421bc5218adb78d98d265e8/portrait.png', kind: 'Doodad'}
+ {slug: 'fence-wall', name: 'Fence Wall', original: '54349179a4cc5c900efa4814', portraitURL: '/file/db/thang.type/54349179a4cc5c900efa4814/portrait.png', kind: 'Doodad'}
+ {slug: 'filing-cabinet', name: 'Filing Cabinet', original: '52e9fa73427172ae56002593', portraitURL: '/file/db/thang.type/52e9fa73427172ae56002593/portrait.png', kind: 'Doodad'}
+ {slug: 'fine-boots', name: 'Fine Boots', original: '53e2388e53457600003e3f09', portraitURL: '/file/db/thang.type/53e2388e53457600003e3f09/portrait.png', kind: 'Item'}
+ {slug: 'fine-leather-chainmail-coif', name: 'Fine Leather Chainmail Coif', original: '546d455f9df4a17d0d449a4f', portraitURL: '/file/db/thang.type/546d455f9df4a17d0d449a4f/portrait.png', kind: 'Item'}
+ {slug: 'fine-leather-chainmail-tunic', name: 'Fine Leather Chainmail Tunic', original: '546d3b129df4a17d0d449a27', portraitURL: '/file/db/thang.type/546d3b129df4a17d0d449a27/portrait.png', kind: 'Item'}
+ {slug: 'fine-stone-builders-hammer', name: 'Fine Stone Builder\'s Hammer', original: '54694c44a2b1f53ce794445a', portraitURL: '/file/db/thang.type/54694c44a2b1f53ce794445a/portrait.png', kind: 'Item'}
+ {slug: 'fine-telephoto-glasses', name: 'Fine Telephoto Glasses', original: '54694194a2b1f53ce7944415', portraitURL: '/file/db/thang.type/54694194a2b1f53ce7944415/portrait.png', kind: 'Item'}
+ {slug: 'fine-wooden-glasses', name: 'Fine Wooden Glasses', original: '5469405ba2b1f53ce7944404', portraitURL: '/file/db/thang.type/5469405ba2b1f53ce7944404/portrait.png', kind: 'Item'}
+ {slug: 'fir-tree-1', name: 'Fir Tree 1', original: '54e9503df54ef5794f354ec1', portraitURL: '/file/db/thang.type/54e9503df54ef5794f354ec1/portrait.png', kind: 'Doodad'}
+ {slug: 'fir-tree-2', name: 'Fir Tree 2', original: '54e95107f54ef5794f354ec5', portraitURL: '/file/db/thang.type/54e95107f54ef5794f354ec5/portrait.png', kind: 'Doodad'}
+ {slug: 'fir-tree-3', name: 'Fir Tree 3', original: '54e9513ff54ef5794f354ec9', portraitURL: '/file/db/thang.type/54e9513ff54ef5794f354ec9/portrait.png', kind: 'Doodad'}
+ {slug: 'fir-tree-4', name: 'Fir Tree 4', original: '54e9518df54ef5794f354ecd', portraitURL: '/file/db/thang.type/54e9518df54ef5794f354ecd/portrait.png', kind: 'Doodad'}
+ {slug: 'fire', name: 'Fire', original: '54d2bdea4e4a08550556dbfe', portraitURL: '/file/db/thang.type/54d2bdea4e4a08550556dbfe/portrait.png', kind: 'Mark'}
+ {slug: 'fire-dancing-background', name: 'Fire Dancing Background', original: '576ad6ab7e64f325002df2e4', portraitURL: '/file/db/thang.type/576ad6ab7e64f325002df2e4/portrait.png', kind: 'Floor'}
+ {slug: 'fire-opal-sense-stone', name: 'Fire Opal Sense Stone', original: '546932e4a2b1f53ce79443cd', portraitURL: '/file/db/thang.type/546932e4a2b1f53ce79443cd/portrait.png', kind: 'Item'}
+ {slug: 'fire-trap', name: 'Fire Trap', original: '5449536afb56d566e86972ba', portraitURL: '/file/db/thang.type/5449536afb56d566e86972ba/portrait.png', kind: 'Misc'}
+ {slug: 'fireball', name: 'Fireball', original: '531a6a2f1ddc910545d5e944', portraitURL: '/file/db/thang.type/531a6a2f1ddc910545d5e944/portrait.png', kind: 'Missile'}
+ {slug: 'firewall', name: 'firewall', original: '56f56687db0216900f086ac1', portraitURL: '/file/db/thang.type/56f56687db0216900f086ac1/portrait.png', kind: 'Doodad'}
+ {slug: 'firewood-1', name: 'Firewood 1', original: '52e953d0427172ae5600181d', portraitURL: '/file/db/thang.type/52e953d0427172ae5600181d/portrait.png', kind: 'Doodad'}
+ {slug: 'firewood-2', name: 'Firewood 2', original: '52e9575d22efc8e7090016ed', portraitURL: '/file/db/thang.type/52e9575d22efc8e7090016ed/portrait.png', kind: 'Doodad'}
+ {slug: 'firewood-3', name: 'Firewood 3', original: '52e957ec22efc8e7090016fd', portraitURL: '/file/db/thang.type/52e957ec22efc8e7090016fd/portrait.png', kind: 'Doodad'}
+ {slug: 'firn-1', name: 'Firn 1', original: '557639fa1e82182d9e68894d', portraitURL: '/file/db/thang.type/557639fa1e82182d9e68894d/portrait.png', kind: 'Floor'}
+ {slug: 'firn-2', name: 'Firn 2', original: '55763a971e82182d9e688951', portraitURL: '/file/db/thang.type/55763a971e82182d9e688951/portrait.png', kind: 'Floor'}
+ {slug: 'firn-3', name: 'Firn 3', original: '55763ab51e82182d9e688955', portraitURL: '/file/db/thang.type/55763ab51e82182d9e688955/portrait.png', kind: 'Floor'}
+ {slug: 'firn-4', name: 'Firn 4', original: '55763ad11e82182d9e688959', portraitURL: '/file/db/thang.type/55763ad11e82182d9e688959/portrait.png', kind: 'Floor'}
+ {slug: 'firn-5', name: 'Firn 5', original: '55763aea1e82182d9e68895d', portraitURL: '/file/db/thang.type/55763aea1e82182d9e68895d/portrait.png', kind: 'Floor'}
+ {slug: 'firn-6', name: 'Firn 6', original: '55763b111e82182d9e688961', portraitURL: '/file/db/thang.type/55763b111e82182d9e688961/portrait.png', kind: 'Floor'}
+ {slug: 'firn-cliff', name: 'Firn Cliff', original: '55c277983767fd3435eb444e', portraitURL: '/file/db/thang.type/55c277983767fd3435eb444e/portrait.png', kind: 'Floor'}
+ {slug: 'flame-armor', name: 'Flame Armor', original: '55c27b5c3767fd3435eb445a', portraitURL: '/file/db/thang.type/55c27b5c3767fd3435eb445a/portrait.png', kind: 'Mark'}
+ {slug: 'flaming-shell', name: 'Flaming Shell', original: '553e80669bdea5d00f1fd90e', portraitURL: '/file/db/thang.type/553e80669bdea5d00f1fd90e/portrait.png', kind: 'Missile'}
+ {slug: 'flippable-land', name: 'Flippable Land', original: '53a20126610a6b3505568163', portraitURL: '/file/db/thang.type/53a20126610a6b3505568163/portrait.png', kind: 'Floor'}
+ {slug: 'floppy-lambswool-hat', name: 'Floppy Lambswool Hat', original: '546d4b069df4a17d0d449aa3', portraitURL: '/file/db/thang.type/546d4b069df4a17d0d449aa3/portrait.png', kind: 'Item'}
+ {slug: 'force-bolt', name: 'Force Bolt', original: '5467807c417c8b48a9811efd', portraitURL: '/file/db/thang.type/5467807c417c8b48a9811efd/portrait.png', kind: 'Missile'}
+ {slug: 'forest-river-tile-deadend', name: 'Forest River tile deadend', original: '577d5b367e0491260074b95b', portraitURL: '/file/db/thang.type/577d5b367e0491260074b95b/portrait.png', kind: 'undefined'}
+ {slug: 'forest-river-tile-full-intersection', name: 'forest river tile full intersection', original: '5786badaa6c6413500926209', portraitURL: '/file/db/thang.type/5786badaa6c6413500926209/portrait.png', kind: 'undefined'}
+ {slug: 'forest-river-tile-straight', name: 'forest river tile straight', original: '577d58d5dbf35b24001b91cb', portraitURL: '/file/db/thang.type/577d58d5dbf35b24001b91cb/portrait.png', kind: 'undefined'}
+ {slug: 'forest-river-tile-t-intersection', name: 'Forest River tile t intersection', original: '577d5b927e0491260074ba3a', portraitURL: '/file/db/thang.type/577d5b927e0491260074ba3a/portrait.png', kind: 'undefined'}
+ {slug: 'forest-river-tile-turn', name: 'Forest River tile turn', original: '577d59e37e0491260074b5bd', portraitURL: '/file/db/thang.type/577d59e37e0491260074b5bd/portrait.png', kind: 'undefined'}
+ {slug: 'forgetful-gemsmith-background', name: 'Forgetful Gemsmith Background', original: '562a9b9ea4cdd48805fb98ca', portraitURL: '/file/db/thang.type/562a9b9ea4cdd48805fb98ca/portrait.png', kind: 'Floor'}
+ {slug: 'forgotten-bronze-ring', name: 'Forgotten Bronze Ring', original: '54692d8aa2b1f53ce7944397', portraitURL: '/file/db/thang.type/54692d8aa2b1f53ce7944397/portrait.png', kind: 'Item'}
+ {slug: 'frog', name: 'Frog', original: '57869cf7bd31c14400834028', portraitURL: '/file/db/thang.type/57869cf7bd31c14400834028/portrait.png', kind: 'Item'}
+ {slug: 'frog-pet', name: 'Frog Pet', original: '540f3678821af8000097dc56', portraitURL: '/file/db/thang.type/540f3678821af8000097dc56/portrait.png', kind: 'Unit'}
+ {slug: 'gargoyle', name: 'Gargoyle', original: '52afc8f0c5b1813ec2000008', portraitURL: '/file/db/thang.type/52afc8f0c5b1813ec2000008/portrait.png', kind: 'Doodad'}
+ {slug: 'gargoyle-side', name: 'Gargoyle Side', original: '54efa07f4bb4788505d2339e', portraitURL: '/file/db/thang.type/54efa07f4bb4788505d2339e/portrait.png', kind: 'Doodad'}
+ {slug: 'gauntlets-of-strength', name: 'Gauntlets of Strength', original: '53e2202953457600003e3ed9', portraitURL: '/file/db/thang.type/53e2202953457600003e3ed9/portrait.png', kind: 'Item'}
+ {slug: 'gem-pile-medium', name: 'Gem Pile Medium', original: '543306638364d30000d1f951', portraitURL: '/file/db/thang.type/543306638364d30000d1f951/portrait.png', kind: 'Misc'}
+ {slug: 'gem-pile-small', name: 'Gem Pile Small', original: '543305f78364d30000d1f94a', portraitURL: '/file/db/thang.type/543305f78364d30000d1f94a/portrait.png', kind: 'Misc'}
+ {slug: 'gems-of-the-deep-background', name: 'Gems of the Deep Background', original: '563aa55276289f86054a7c02', portraitURL: '/file/db/thang.type/563aa55276289f86054a7c02/portrait.png', kind: 'Floor'}
+ {slug: 'generic-armor-mark-1', name: 'Generic Armor Mark 1', original: '54d2be25bb157252059b2202', portraitURL: '/file/db/thang.type/54d2be25bb157252059b2202/portrait.png', kind: 'Mark'}
+ {slug: 'generic-armor-mark-2', name: 'Generic Armor Mark 2', original: '54d2be9e3e16915505f0c7a4', portraitURL: '/file/db/thang.type/54d2be9e3e16915505f0c7a4/portrait.png', kind: 'Mark'}
+ {slug: 'generic-item', name: 'Generic Item', original: '545d3eb52d03e700001b5a5b', portraitURL: '/file/db/thang.type/545d3eb52d03e700001b5a5b/portrait.png', kind: 'Item'}
+ {slug: 'gift-of-the-trees', name: 'Gift of the Trees', original: '54eab0a32b7506e891ca71dd', portraitURL: '/file/db/thang.type/54eab0a32b7506e891ca71dd/portrait.png', kind: 'Item'}
+ {slug: 'gilt-wristwatch', name: 'Gilt Wristwatch', original: '54693830a2b1f53ce79443f1', portraitURL: '/file/db/thang.type/54693830a2b1f53ce79443f1/portrait.png', kind: 'Item'}
+ {slug: 'glasses-doodad', name: 'Glasses Doodad', original: '5420c4c5a0feb36ad21d45e2', portraitURL: '/file/db/thang.type/5420c4c5a0feb36ad21d45e2/portrait.png', kind: 'Item'}
+ {slug: 'glitterbomb', name: 'Glitterbomb', original: '54eb50f649fa2d5c905ddf0a', portraitURL: '/file/db/thang.type/54eb50f649fa2d5c905ddf0a/portrait.png', kind: 'Item'}
+ {slug: 'goal-trigger', name: 'Goal Trigger', original: '52bcbf0dce43b70000000006', portraitURL: '/file/db/thang.type/52bcbf0dce43b70000000006/portrait.png', kind: 'Misc'}
+ {slug: 'gold-ball', name: 'Gold Ball', original: '550b742b8a7d3c197a824dad', portraitURL: '/file/db/thang.type/550b742b8a7d3c197a824dad/portrait.png', kind: 'Missile'}
+ {slug: 'gold-cloud', name: 'Gold Cloud', original: '550b4b9d8a7d3c197a824d5e', portraitURL: '/file/db/thang.type/550b4b9d8a7d3c197a824d5e/portrait.png', kind: 'Doodad'}
+ {slug: 'golden-wand', name: 'Golden Wand', original: '54eab7f52b7506e891ca7202', portraitURL: '/file/db/thang.type/54eab7f52b7506e891ca7202/portrait.png', kind: 'Item'}
+ {slug: 'goldspun-silk-cloak', name: 'Goldspun Silk Cloak', original: '546d49da9df4a17d0d449a8f', portraitURL: '/file/db/thang.type/546d49da9df4a17d0d449a8f/portrait.png', kind: 'Item'}
+ {slug: 'goldspun-silk-hat', name: 'Goldspun Silk Hat', original: '546d4c249df4a17d0d449ab7', portraitURL: '/file/db/thang.type/546d4c249df4a17d0d449ab7/portrait.png', kind: 'Item'}
+ {slug: 'grass-cliffs', name: 'Grass Cliffs', original: '52bcb96ece43b70000000003', portraitURL: '/file/db/thang.type/52bcb96ece43b70000000003/portrait.png', kind: 'Floor'}
+ {slug: 'grass01', name: 'Grass01', original: '53016dddd82649ec2c0c9b29', portraitURL: '/file/db/thang.type/53016dddd82649ec2c0c9b29/portrait.png', kind: 'Floor'}
+ {slug: 'grass02', name: 'Grass02', original: '53016fc098f2ca1f6e82eebd', portraitURL: '/file/db/thang.type/53016fc098f2ca1f6e82eebd/portrait.png', kind: 'Floor'}
+ {slug: 'grass03', name: 'Grass03', original: '5301702d98f2ca1f6e82eec4', portraitURL: '/file/db/thang.type/5301702d98f2ca1f6e82eec4/portrait.png', kind: 'Floor'}
+ {slug: 'grass04', name: 'Grass04', original: '530170a198f2ca1f6e82eecf', portraitURL: '/file/db/thang.type/530170a198f2ca1f6e82eecf/portrait.png', kind: 'Floor'}
+ {slug: 'grass05', name: 'Grass05', original: '5301716398f2ca1f6e82eedc', portraitURL: '/file/db/thang.type/5301716398f2ca1f6e82eedc/portrait.png', kind: 'Floor'}
+ {slug: 'gravestone-cross', name: 'Gravestone Cross', original: '54f1100e8d380d7f05acc975', portraitURL: '/file/db/thang.type/54f1100e8d380d7f05acc975/portrait.png', kind: 'Doodad'}
+ {slug: 'gravestone-rounded', name: 'Gravestone Rounded', original: '54f110e3f854c97a05551616', portraitURL: '/file/db/thang.type/54f110e3f854c97a05551616/portrait.png', kind: 'Doodad'}
+ {slug: 'gravestone-square', name: 'Gravestone Square', original: '54f10f08d2969f8405ef51fd', portraitURL: '/file/db/thang.type/54f10f08d2969f8405ef51fd/portrait.png', kind: 'Doodad'}
+ {slug: 'graveyard-fence', name: 'Graveyard Fence', original: '54f111b379054c8705757747', portraitURL: '/file/db/thang.type/54f111b379054c8705757747/portrait.png', kind: 'Doodad'}
+ {slug: 'great-sword', name: 'Great Sword', original: '544d7f8d8494308424f564bf', portraitURL: '/file/db/thang.type/544d7f8d8494308424f564bf/portrait.png', kind: 'Item'}
+ {slug: 'greed-background', name: 'Greed Background', original: '53764e4ea7b5ab3805f153a4', portraitURL: '/file/db/thang.type/53764e4ea7b5ab3805f153a4/portrait.png', kind: 'Floor'}
+ {slug: 'green-bubble-missile', name: 'Green Bubble Missile', original: '540e35a34f21cd879ba4f140', portraitURL: '/file/db/thang.type/540e35a34f21cd879ba4f140/portrait.png', kind: 'Missile'}
+ {slug: 'griffin-rider', name: 'Griffin Rider', original: '52d45d1ab10ae4b024000002', portraitURL: '/file/db/thang.type/52d45d1ab10ae4b024000002/portrait.png', kind: 'Unit'}
+ {slug: 'griffin-wool-hat', name: 'Griffin Wool Hat', original: '546d4c699df4a17d0d449abb', portraitURL: '/file/db/thang.type/546d4c699df4a17d0d449abb/portrait.png', kind: 'Item'}
+ {slug: 'griffin-wool-robe', name: 'Griffin Wool Robe', original: '546d4a159df4a17d0d449a93', portraitURL: '/file/db/thang.type/546d4a159df4a17d0d449a93/portrait.png', kind: 'Item'}
+ {slug: 'hand-sewn-linen-wizards-hat', name: 'Hand-sewn Linen Wizard\'s Hat', original: '546d4bec9df4a17d0d449ab3', portraitURL: '/file/db/thang.type/546d4bec9df4a17d0d449ab3/portrait.png', kind: 'Item'}
+ {slug: 'hardened-emerald-chainmail-coif', name: 'Hardened Emerald Chainmail Coif', original: '546d47159df4a17d0d449a67', portraitURL: '/file/db/thang.type/546d47159df4a17d0d449a67/portrait.png', kind: 'Item'}
+ {slug: 'hardened-emerald-chainmail-tunic', name: 'Hardened Emerald Chainmail Tunic', original: '546d3cce9df4a17d0d449a3f', portraitURL: '/file/db/thang.type/546d3cce9df4a17d0d449a3f/portrait.png', kind: 'Item'}
+ {slug: 'hardened-steel-glasses', name: 'Hardened Steel Glasses', original: '546940d8a2b1f53ce794440d', portraitURL: '/file/db/thang.type/546940d8a2b1f53ce794440d/portrait.png', kind: 'Item'}
+ {slug: 'harrowland-background', name: 'Harrowland Background', original: '572e51e3f8c4f9b601ede885', portraitURL: '/file/db/thang.type/572e51e3f8c4f9b601ede885/portrait.png', kind: 'Floor'}
+ {slug: 'haste', name: 'Haste', original: '530251cfa6efdd32359c53d5', portraitURL: '/file/db/thang.type/530251cfa6efdd32359c53d5/portrait.png', kind: 'Mark'}
+ {slug: 'haunted-kithmaze-background', name: 'Haunted Kithmaze Background', original: '569dd4f2b55fd82e0011b79b', portraitURL: '/file/db/thang.type/569dd4f2b55fd82e0011b79b/portrait.png', kind: 'Floor'}
+ {slug: 'heal', name: 'Heal', original: '55c63ebcef141c65665beb59', portraitURL: '/file/db/thang.type/55c63ebcef141c65665beb59/portrait.png', kind: 'Mark'}
+ {slug: 'health-potion-large', name: 'Health Potion Large', original: '52afc634c5b1813ec2000002', portraitURL: '/file/db/thang.type/52afc634c5b1813ec2000002/portrait.png', kind: 'Misc'}
+ {slug: 'health-potion-medium', name: 'Health Potion Medium', original: '52afc742c5b1813ec2000004', portraitURL: '/file/db/thang.type/52afc742c5b1813ec2000004/portrait.png', kind: 'Misc'}
+ {slug: 'health-potion-small', name: 'Health Potion Small', original: '52afc7b6c5b1813ec2000006', portraitURL: '/file/db/thang.type/52afc7b6c5b1813ec2000006/portrait.png', kind: 'Misc'}
+ {slug: 'heavy-iron-breastplate', name: 'Heavy Iron Breastplate', original: '546aaf1b3777d6186329285e', portraitURL: '/file/db/thang.type/546aaf1b3777d6186329285e/portrait.png', kind: 'Item'}
+ {slug: 'heavy-iron-helmet', name: 'Heavy Iron Helmet', original: '546d390b9df4a17d0d449a0b', portraitURL: '/file/db/thang.type/546d390b9df4a17d0d449a0b/portrait.png', kind: 'Item'}
+ {slug: 'helmet-fall-1', name: 'Helmet Fall 1', original: '53e2e3e66c59f5340504108f', portraitURL: '/file/db/thang.type/53e2e3e66c59f5340504108f/portrait.png', kind: 'Doodad'}
+ {slug: 'hide', name: 'Hide', original: '55c281e83767fd3435eb446d', portraitURL: '/file/db/thang.type/55c281e83767fd3435eb446d/portrait.png', kind: 'Mark'}
+ {slug: 'highlight', name: 'Highlight', original: '529f8fdbdacd325127000003', portraitURL: '/file/db/thang.type/529f8fdbdacd325127000003/portrait.png', kind: 'Mark'}
+ {slug: 'holoball', name: 'Holoball', original: '56d0fa8a087ee32400764bb8', portraitURL: '/file/db/thang.type/56d0fa8a087ee32400764bb8/portrait.png', kind: 'Doodad'}
+ {slug: 'holy-sword', name: 'Holy Sword', original: '53e21249b82921000051ce11', portraitURL: '/file/db/thang.type/53e21249b82921000051ce11/portrait.png', kind: 'Item'}
+ {slug: 'house-1', name: 'House 1', original: '52b095bbccbc671372000006', portraitURL: '/file/db/thang.type/52b095bbccbc671372000006/portrait.png', kind: 'Doodad'}
+ {slug: 'house-2', name: 'House 2', original: '52b09d35ccbc671372000009', portraitURL: '/file/db/thang.type/52b09d35ccbc671372000009/portrait.png', kind: 'Doodad'}
+ {slug: 'house-3', name: 'House 3', original: '52b09dd0ccbc67137200000b', portraitURL: '/file/db/thang.type/52b09dd0ccbc67137200000b/portrait.png', kind: 'Doodad'}
+ {slug: 'house-4', name: 'House 4', original: '52b09e2fccbc67137200000d', portraitURL: '/file/db/thang.type/52b09e2fccbc67137200000d/portrait.png', kind: 'Doodad'}
+ {slug: 'hoverboard-stand', name: 'Hoverboard Stand', original: '56c630aeed946a44004ff139', portraitURL: '/file/db/thang.type/56c630aeed946a44004ff139/portrait.png', kind: 'Doodad'}
+ {slug: 'human-barracks', name: 'Human Barracks', original: '530ce329ec5bdaba2a72a99c', portraitURL: '/file/db/thang.type/530ce329ec5bdaba2a72a99c/portrait.png', kind: 'Unit'}
+ {slug: 'hunting-rifle', name: 'Hunting Rifle', original: '544d82aa8494308424f564cf', portraitURL: '/file/db/thang.type/544d82aa8494308424f564cf/portrait.png', kind: 'Item'}
+ {slug: 'ice-crystals-1', name: 'Ice Crystals 1', original: '557639501e82182d9e688945', portraitURL: '/file/db/thang.type/557639501e82182d9e688945/portrait.png', kind: 'Doodad'}
+ {slug: 'ice-crystals-2', name: 'Ice Crystals 2', original: '557639b91e82182d9e688949', portraitURL: '/file/db/thang.type/557639b91e82182d9e688949/portrait.png', kind: 'Doodad'}
+ {slug: 'ice-door', name: 'Ice Door', original: '557f32b0b43ce0b15a91b16d', portraitURL: '/file/db/thang.type/557f32b0b43ce0b15a91b16d/portrait.png', kind: 'Doodad'}
+ {slug: 'ice-gargoyle', name: 'Ice Gargoyle', original: '55760d3f1e82182d9e6888f6', portraitURL: '/file/db/thang.type/55760d3f1e82182d9e6888f6/portrait.png', kind: 'Doodad'}
+ {slug: 'ice-gargoyle-fore', name: 'Ice Gargoyle Fore', original: '55760e311e82182d9e688902', portraitURL: '/file/db/thang.type/55760e311e82182d9e688902/portrait.png', kind: 'Doodad'}
+ {slug: 'ice-gargoyle-ruin', name: 'Ice Gargoyle Ruin', original: '55760dc31e82182d9e6888fa', portraitURL: '/file/db/thang.type/55760dc31e82182d9e6888fa/portrait.png', kind: 'Doodad'}
+ {slug: 'ice-gargoyle-ruin-fore', name: 'Ice Gargoyle Ruin Fore', original: '55760dfa1e82182d9e6888fe', portraitURL: '/file/db/thang.type/55760dfa1e82182d9e6888fe/portrait.png', kind: 'Doodad'}
+ {slug: 'ice-rink-1', name: 'Ice Rink 1', original: '557f321bb43ce0b15a91b161', portraitURL: '/file/db/thang.type/557f321bb43ce0b15a91b161/portrait.png', kind: 'Floor'}
+ {slug: 'ice-rink-2', name: 'Ice Rink 2', original: '557f325cb43ce0b15a91b165', portraitURL: '/file/db/thang.type/557f325cb43ce0b15a91b165/portrait.png', kind: 'Floor'}
+ {slug: 'ice-rink-3', name: 'Ice Rink 3', original: '557f3275b43ce0b15a91b169', portraitURL: '/file/db/thang.type/557f3275b43ce0b15a91b169/portrait.png', kind: 'Floor'}
+ {slug: 'ice-tree-1', name: 'Ice Tree 1', original: '557635641e82182d9e688929', portraitURL: '/file/db/thang.type/557635641e82182d9e688929/portrait.png', kind: 'Doodad'}
+ {slug: 'ice-tree-2', name: 'Ice Tree 2', original: '557636401e82182d9e68892d', portraitURL: '/file/db/thang.type/557636401e82182d9e68892d/portrait.png', kind: 'Doodad'}
+ {slug: 'ice-tree-3', name: 'Ice Tree 3', original: '557636e11e82182d9e688931', portraitURL: '/file/db/thang.type/557636e11e82182d9e688931/portrait.png', kind: 'Doodad'}
+ {slug: 'ice-wall', name: 'Ice Wall', original: '5575f002f3f8d13b4ee1e7fc', portraitURL: '/file/db/thang.type/5575f002f3f8d13b4ee1e7fc/portrait.png', kind: 'Wall'}
+ {slug: 'ice-yak', name: 'Ice Yak', original: '557f3917b43ce0b15a91b175', portraitURL: '/file/db/thang.type/557f3917b43ce0b15a91b175/portrait.png', kind: 'Unit'}
+ {slug: 'igloo-1', name: 'Igloo 1', original: '557608f61e82182d9e6888cf', portraitURL: '/file/db/thang.type/557608f61e82182d9e6888cf/portrait.png', kind: 'Doodad'}
+ {slug: 'igloo-2', name: 'Igloo 2', original: '557609b01e82182d9e6888d3', portraitURL: '/file/db/thang.type/557609b01e82182d9e6888d3/portrait.png', kind: 'Doodad'}
+ {slug: 'igloo-3', name: 'Igloo 3', original: '557609dd1e82182d9e6888d7', portraitURL: '/file/db/thang.type/557609dd1e82182d9e6888d7/portrait.png', kind: 'Doodad'}
+ {slug: 'igloo-4', name: 'Igloo 4', original: '55760a2c1e82182d9e6888db', portraitURL: '/file/db/thang.type/55760a2c1e82182d9e6888db/portrait.png', kind: 'Doodad'}
+ {slug: 'impaling-firebolt', name: 'Impaling Firebolt', original: '54f767c4b3e4927805021022', portraitURL: '/file/db/thang.type/54f767c4b3e4927805021022/portrait.png', kind: 'Missile'}
+ {slug: 'importer-of-great-justice', name: 'Importer of Great Justice', original: '54938575e9850ae3e8fbdd74', portraitURL: '/file/db/thang.type/54938575e9850ae3e8fbdd74/portrait.png', kind: 'undefined'}
+ {slug: 'indoor-floor', name: 'Indoor Floor', original: '52ead2b2207133f35c000833', portraitURL: '/file/db/thang.type/52ead2b2207133f35c000833/portrait.png', kind: 'Floor'}
+ {slug: 'indoor-wall', name: 'Indoor Wall', original: '52ea9a13d23f140d100000b2', portraitURL: '/file/db/thang.type/52ea9a13d23f140d100000b2/portrait.png', kind: 'Wall'}
+ {slug: 'infantry-shield', name: 'Infantry Shield', original: '544d7bb88494308424f56493', portraitURL: '/file/db/thang.type/544d7bb88494308424f56493/portrait.png', kind: 'Item'}
+ {slug: 'invisible', name: 'Invisible', original: '52b0f9c75c5c4af6bd000004', portraitURL: '/file/db/thang.type/52b0f9c75c5c4af6bd000004/portrait.png', kind: 'Misc'}
+ {slug: 'iron-chainmail-coif', name: 'Iron Chainmail Coif', original: '546d45c59df4a17d0d449a53', portraitURL: '/file/db/thang.type/546d45c59df4a17d0d449a53/portrait.png', kind: 'Item'}
+ {slug: 'iron-chainmail-tunic', name: 'Iron Chainmail Tunic', original: '546d3b7c9df4a17d0d449a2b', portraitURL: '/file/db/thang.type/546d3b7c9df4a17d0d449a2b/portrait.png', kind: 'Item'}
+ {slug: 'iron-defender', name: 'Iron Defender', original: '54eaabe62b7506e891ca71c9', portraitURL: '/file/db/thang.type/54eaabe62b7506e891ca71c9/portrait.png', kind: 'Item'}
+ {slug: 'iron-link', name: 'Iron Link', original: '54692d5ca2b1f53ce7944393', portraitURL: '/file/db/thang.type/54692d5ca2b1f53ce7944393/portrait.png', kind: 'Item'}
+ {slug: 'iron-maiden', name: 'Iron Maiden', original: '54ef9f0a83b08b7d054ba50d', portraitURL: '/file/db/thang.type/54ef9f0a83b08b7d054ba50d/portrait.png', kind: 'Doodad'}
+ {slug: 'iron-shield', name: 'Iron Shield', original: '5441c3f44e9aeb727cc97129', portraitURL: '/file/db/thang.type/5441c3f44e9aeb727cc97129/portrait.png', kind: 'Item'}
+ {slug: 'kings-ring', name: 'King\'s Ring', original: '54eb56df49fa2d5c905ddf2e', portraitURL: '/file/db/thang.type/54eb56df49fa2d5c905ddf2e/portrait.png', kind: 'Item'}
+ {slug: 'kithgard-gates-background', name: 'Kithgard Gates Background', original: '572e52b17a9c3e8101b8be0e', portraitURL: '/file/db/thang.type/572e52b17a9c3e8101b8be0e/portrait.png', kind: 'Floor'}
+ {slug: 'kithgard-workers-glasses', name: 'Kithgard Worker\'s Glasses', original: '53eb99f41a100989a40ce46e', portraitURL: '/file/db/thang.type/53eb99f41a100989a40ce46e/portrait.png', kind: 'Item'}
+ {slug: 'kithsteel-blade', name: 'Kithsteel Blade', original: '54eaa78a2b7506e891ca719d', portraitURL: '/file/db/thang.type/54eaa78a2b7506e891ca719d/portrait.png', kind: 'Item'}
+ {slug: 'knightfire-charge', name: 'Knightfire Charge', original: '544d96328494308424f56533', portraitURL: '/file/db/thang.type/544d96328494308424f56533/portrait.png', kind: 'Item'}
+ {slug: 'knightfire-charge-missile', name: 'Knightfire Charge Missile', original: '546297f1f44055a4b5e735bb', portraitURL: '/file/db/thang.type/546297f1f44055a4b5e735bb/portrait.png', kind: 'Missile'}
+ {slug: 'known-enemy-background', name: 'Known Enemy Background', original: '572e4e61b2088976012429eb', portraitURL: '/file/db/thang.type/572e4e61b2088976012429eb/portrait.png', kind: 'Floor'}
+ {slug: 'koraths-promise', name: 'Korath\'s Promise', original: '54eb575749fa2d5c905ddf3a', portraitURL: '/file/db/thang.type/54eb575749fa2d5c905ddf3a/portrait.png', kind: 'Item'}
+ {slug: 'krummholz-1', name: 'Krummholz 1', original: '54e953adf54ef5794f354ef1', portraitURL: '/file/db/thang.type/54e953adf54ef5794f354ef1/portrait.png', kind: 'Doodad'}
+ {slug: 'krummholz-2', name: 'Krummholz 2', original: '54e9545bf54ef5794f354ef5', portraitURL: '/file/db/thang.type/54e9545bf54ef5794f354ef5/portrait.png', kind: 'Doodad'}
+ {slug: 'krummholz-3', name: 'Krummholz 3', original: '54e95492f54ef5794f354ef9', portraitURL: '/file/db/thang.type/54e95492f54ef5794f354ef9/portrait.png', kind: 'Doodad'}
+ {slug: 'lambswool-cloak', name: 'Lambswool Cloak', original: '546d48ce9df4a17d0d449a7b', portraitURL: '/file/db/thang.type/546d48ce9df4a17d0d449a7b/portrait.png', kind: 'Item'}
+ {slug: 'large-bolt-crossbow', name: 'Large Bolt Crossbow', original: '544d80598494308424f564c7', portraitURL: '/file/db/thang.type/544d80598494308424f564c7/portrait.png', kind: 'Item'}
+ {slug: 'large-classroom-viewscreen-off', name: 'Large Classroom Viewscreen Off', original: '56c632c6abf4a61f009040b5', portraitURL: '/file/db/thang.type/56c632c6abf4a61f009040b5/portrait.png', kind: 'Doodad'}
+ {slug: 'large-classroom-viewscreen-on', name: 'Large Classroom Viewscreen On', original: '56eb28b267a0142000a36358', portraitURL: '/file/db/thang.type/56eb28b267a0142000a36358/portrait.png', kind: 'Doodad'}
+ {slug: 'lava-grate', name: 'Lava Grate', original: '54ef9fb8c1f3bd7c05941750', portraitURL: '/file/db/thang.type/54ef9fb8c1f3bd7c05941750/portrait.png', kind: 'Doodad'}
+ {slug: 'leather-belt', name: 'Leather Belt', original: '5437002a7beba4a82024a97d', portraitURL: '/file/db/thang.type/5437002a7beba4a82024a97d/portrait.png', kind: 'Item'}
+ {slug: 'leather-boots', name: 'Leather Boots', original: '53e2384453457600003e3f07', portraitURL: '/file/db/thang.type/53e2384453457600003e3f07/portrait.png', kind: 'Item'}
+ {slug: 'leather-chainmail-coif', name: 'Leather Chainmail Coif', original: '546d45089df4a17d0d449a4b', portraitURL: '/file/db/thang.type/546d45089df4a17d0d449a4b/portrait.png', kind: 'Item'}
+ {slug: 'leather-chainmail-tunic', name: 'Leather Chainmail Tunic', original: '546d3ab69df4a17d0d449a23', portraitURL: '/file/db/thang.type/546d3ab69df4a17d0d449a23/portrait.png', kind: 'Item'}
+ {slug: 'leather-tunic', name: 'Leather Tunic', original: '545d3cf22d03e700001b5a58', portraitURL: '/file/db/thang.type/545d3cf22d03e700001b5a58/portrait.png', kind: 'Item'}
+ {slug: 'level-banner', name: 'Level Banner', original: '5432c9688364d30000d1f935', portraitURL: '/file/db/thang.type/5432c9688364d30000d1f935/portrait.png', kind: 'Misc'}
+ {slug: 'lightning-bolt', name: 'Lightning Bolt', original: '54f3fa515fcc6a3950c7eabd', portraitURL: '/file/db/thang.type/54f3fa515fcc6a3950c7eabd/portrait.png', kind: 'Missile'}
+ {slug: 'lightning-stick', name: 'Lightning Stick', original: '544d86318494308424f564e8', portraitURL: '/file/db/thang.type/544d86318494308424f564e8/portrait.png', kind: 'Item'}
+ {slug: 'lightning-twig', name: 'Lightning Twig', original: '54eab1ec2b7506e891ca71e1', portraitURL: '/file/db/thang.type/54eab1ec2b7506e891ca71e1/portrait.png', kind: 'Item'}
+ {slug: 'lightstone', name: 'Lightstone', original: '54da20b7163110520551ed33', portraitURL: '/file/db/thang.type/54da20b7163110520551ed33/portrait.png', kind: 'Misc'}
+ {slug: 'log-1', name: 'Log 1', original: '54e954d7f54ef5794f354efd', portraitURL: '/file/db/thang.type/54e954d7f54ef5794f354efd/portrait.png', kind: 'Doodad'}
+ {slug: 'log-2', name: 'Log 2', original: '54e9553ef54ef5794f354f01', portraitURL: '/file/db/thang.type/54e9553ef54ef5794f354f01/portrait.png', kind: 'Doodad'}
+ {slug: 'log-3', name: 'Log 3', original: '54e9556af54ef5794f354f05', portraitURL: '/file/db/thang.type/54e9556af54ef5794f354f05/portrait.png', kind: 'Doodad'}
+ {slug: 'long-sword', name: 'Long Sword', original: '544d7d1f8494308424f564a3', portraitURL: '/file/db/thang.type/544d7d1f8494308424f564a3/portrait.png', kind: 'Item'}
+ {slug: 'loop-da-loop-background', name: 'Loop Da Loop Background', original: '56c67cba797353370060506d', portraitURL: '/file/db/thang.type/56c67cba797353370060506d/portrait.png', kind: 'Floor'}
+ {slug: 'magic-missile', name: 'Magic Missile', original: '5467beaf69d1ba0000fb91fb', portraitURL: '/file/db/thang.type/5467beaf69d1ba0000fb91fb/portrait.png', kind: 'Missile'}
+ {slug: 'magnetize', name: 'Magnetize', original: '55c6403eef141c65665beb5e', portraitURL: '/file/db/thang.type/55c6403eef141c65665beb5e/portrait.png', kind: 'Mark'}
+ {slug: 'mahogany-glasses', name: 'Mahogany Glasses', original: '54694093a2b1f53ce7944408', portraitURL: '/file/db/thang.type/54694093a2b1f53ce7944408/portrait.png', kind: 'Item'}
+ {slug: 'mahogany-staff', name: 'Mahogany Staff', original: '544d88158494308424f56501', portraitURL: '/file/db/thang.type/544d88158494308424f56501/portrait.png', kind: 'Item'}
+ {slug: 'maka-test-wall', name: 'maka-test-wall', original: '56a7d85fb679392600e31138', portraitURL: '/file/db/thang.type/56a7d85fb679392600e31138/portrait.png', kind: 'Wall'}
+ {slug: 'market-stand', name: 'Market Stand', original: '54f11600f854c97a055516da', portraitURL: '/file/db/thang.type/54f11600f854c97a055516da/portrait.png', kind: 'Doodad'}
+ {slug: 'master-of-names-background', name: 'Master of Names Background', original: '572e4ec2e8db519501484869', portraitURL: '/file/db/thang.type/572e4ec2e8db519501484869/portrait.png', kind: 'Floor'}
+ {slug: 'master-sword', name: 'Master Sword', original: '54ea89112b7506e891ca717d', portraitURL: '/file/db/thang.type/54ea89112b7506e891ca717d/portrait.png', kind: 'Item'}
+ {slug: 'masters-flags', name: 'Master\'s Flags', original: '5478b9be8707a2c3a2493b33', portraitURL: '/file/db/thang.type/5478b9be8707a2c3a2493b33/portrait.png', kind: 'Item'}
+ {slug: 'mausoleum', name: 'Mausoleum', original: '54f1128a25be5e8805837491', portraitURL: '/file/db/thang.type/54f1128a25be5e8805837491/portrait.png', kind: 'Doodad'}
+ {slug: 'mayhem-background', name: 'Mayhem Background', original: '572e5071e8db519501484896', portraitURL: '/file/db/thang.type/572e5071e8db519501484896/portrait.png', kind: 'Floor'}
+ {slug: 'mcp', name: 'mcp', original: '576322da0d81132500afdc8d', portraitURL: '/file/db/thang.type/576322da0d81132500afdc8d/portrait.png', kind: 'Unit'}
+ {slug: 'megaphone', name: 'Megaphone', original: '53e216ff53457600003e3eb7', portraitURL: '/file/db/thang.type/53e216ff53457600003e3eb7/portrait.png', kind: 'Item'}
+ {slug: 'metal-builders-hammer', name: 'Metal Builder\'s Hammer', original: '54694c79a2b1f53ce794445e', portraitURL: '/file/db/thang.type/54694c79a2b1f53ce794445e/portrait.png', kind: 'Item'}
+ {slug: 'moonless-night', name: 'Moonless Night', original: '54692f44a2b1f53ce79443b8', portraitURL: '/file/db/thang.type/54692f44a2b1f53ce79443b8/portrait.png', kind: 'Item'}
+ {slug: 'moonlit-blade', name: 'Moonlit Blade', original: '544d95a48494308424f56523', portraitURL: '/file/db/thang.type/544d95a48494308424f56523/portrait.png', kind: 'Item'}
+ {slug: 'moonlit-blade-missile', name: 'Moonlit Blade Missile', original: '544d97bc8494308424f5653c', portraitURL: '/file/db/thang.type/544d97bc8494308424f5653c/portrait.png', kind: 'Missile'}
+ {slug: 'mornings-edge', name: 'Morning\'s Edge', original: '54eaa69a2b7506e891ca7195', portraitURL: '/file/db/thang.type/54eaa69a2b7506e891ca7195/portrait.png', kind: 'Item'}
+ {slug: 'mountain-1', name: 'Mountain 1', original: '54e931d7970f0b0a263c03ef', portraitURL: '/file/db/thang.type/54e931d7970f0b0a263c03ef/portrait.png', kind: 'Doodad'}
+ {slug: 'mountain-2', name: 'Mountain 2', original: '54e9340b970f0b0a263c03f3', portraitURL: '/file/db/thang.type/54e9340b970f0b0a263c03f3/portrait.png', kind: 'Doodad'}
+ {slug: 'mountain-3', name: 'Mountain 3', original: '54e935d1970f0b0a263c03f7', portraitURL: '/file/db/thang.type/54e935d1970f0b0a263c03f7/portrait.png', kind: 'Doodad'}
+ {slug: 'mountain-4', name: 'Mountain 4', original: '54e9377e970f0b0a263c03fc', portraitURL: '/file/db/thang.type/54e9377e970f0b0a263c03fc/portrait.png', kind: 'Doodad'}
+ {slug: 'mountain-lake-1', name: 'Mountain Lake 1', original: '54e93f0ef54ef5794f354e99', portraitURL: '/file/db/thang.type/54e93f0ef54ef5794f354e99/portrait.png', kind: 'Doodad'}
+ {slug: 'mountain-lake-2', name: 'Mountain Lake 2', original: '54e94106f54ef5794f354ea3', portraitURL: '/file/db/thang.type/54e94106f54ef5794f354ea3/portrait.png', kind: 'Doodad'}
+ {slug: 'mountain-shrub-1', name: 'Mountain Shrub 1', original: '54e9567ff54ef5794f354f11', portraitURL: '/file/db/thang.type/54e9567ff54ef5794f354f11/portrait.png', kind: 'Doodad'}
+ {slug: 'mountain-shrub-2', name: 'Mountain Shrub 2', original: '54e956b7f54ef5794f354f15', portraitURL: '/file/db/thang.type/54e956b7f54ef5794f354f15/portrait.png', kind: 'Doodad'}
+ {slug: 'mountain-shrub-3', name: 'Mountain Shrub 3', original: '54e956def54ef5794f354f19', portraitURL: '/file/db/thang.type/54e956def54ef5794f354f19/portrait.png', kind: 'Doodad'}
+ {slug: 'mountain-shrub-4', name: 'Mountain Shrub 4', original: '54e95724f54ef5794f354f1d', portraitURL: '/file/db/thang.type/54e95724f54ef5794f354f1d/portrait.png', kind: 'Doodad'}
+ {slug: 'mountain-tree-stand-1', name: 'Mountain Tree Stand 1', original: '55c24e91dfc8d0b576e60a5e', portraitURL: '/file/db/thang.type/55c24e91dfc8d0b576e60a5e/portrait.png', kind: 'Doodad'}
+ {slug: 'mountain-tree-stand-2', name: 'Mountain Tree Stand 2', original: '55c25141dfc8d0b576e60a64', portraitURL: '/file/db/thang.type/55c25141dfc8d0b576e60a64/portrait.png', kind: 'Doodad'}
+ {slug: 'mountain-tree-stand-3', name: 'Mountain Tree Stand 3', original: '55c25173dfc8d0b576e60a6a', portraitURL: '/file/db/thang.type/55c25173dfc8d0b576e60a6a/portrait.png', kind: 'Doodad'}
+ {slug: 'mountain-tree-stand-4', name: 'Mountain Tree Stand 4', original: '55c25190dfc8d0b576e60a70', portraitURL: '/file/db/thang.type/55c25190dfc8d0b576e60a70/portrait.png', kind: 'Doodad'}
+ {slug: 'movement-stone', name: 'Movement Stone', original: '546e257a9df4a17d0d449bd9', portraitURL: '/file/db/thang.type/546e257a9df4a17d0d449bd9/portrait.png', kind: 'Doodad'}
+ {slug: 'movement-stone-loop', name: 'Movement Stone Loop', original: '546e24679df4a17d0d449bc1', portraitURL: '/file/db/thang.type/546e24679df4a17d0d449bc1/portrait.png', kind: 'Doodad'}
+ {slug: 'multiplayer-treasure-grove-background', name: 'Multiplayer Treasure Grove Background', original: '572e526e7a9c3e8101b8be02', portraitURL: '/file/db/thang.type/572e526e7a9c3e8101b8be02/portrait.png', kind: 'Floor'}
+ {slug: 'mummy', name: 'Mummy', original: '54ef799c8d75558205e98a8e', portraitURL: '/file/db/thang.type/54ef799c8d75558205e98a8e/portrait.png', kind: 'Doodad'}
+ {slug: 'mushroom', name: 'Mushroom', original: '52bcc23a8c4289607b00000a', portraitURL: '/file/db/thang.type/52bcc23a8c4289607b00000a/portrait.png', kind: 'Misc'}
+ {slug: 'mushroom-cluster-1', name: 'Mushroom Cluster 1', original: '5576376f1e82182d9e688935', portraitURL: '/file/db/thang.type/5576376f1e82182d9e688935/portrait.png', kind: 'Doodad'}
+ {slug: 'mushroom-cluster-2', name: 'Mushroom Cluster 2', original: '557638341e82182d9e688939', portraitURL: '/file/db/thang.type/557638341e82182d9e688939/portrait.png', kind: 'Doodad'}
+ {slug: 'mushroom-cluster-3', name: 'Mushroom Cluster 3', original: '557638731e82182d9e68893d', portraitURL: '/file/db/thang.type/557638731e82182d9e68893d/portrait.png', kind: 'Doodad'}
+ {slug: 'mushroom-cluster-4', name: 'Mushroom Cluster 4', original: '5576390e1e82182d9e688941', portraitURL: '/file/db/thang.type/5576390e1e82182d9e688941/portrait.png', kind: 'Doodad'}
+ {slug: 'musty-linen-robe', name: 'Musty Linen Robe', original: '546d49409df4a17d0d449a83', portraitURL: '/file/db/thang.type/546d49409df4a17d0d449a83/portrait.png', kind: 'Item'}
+ {slug: 'newmakatesthushbaum', name: 'newmakatesthushbaum', original: '56ce223647c33f2400d98c66', portraitURL: '/file/db/thang.type/56ce223647c33f2400d98c66/portrait.png', kind: 'undefined'}
+ {slug: 'nightingales-song', name: 'Nightingale\'s Song', original: '54eb570b49fa2d5c905ddf32', portraitURL: '/file/db/thang.type/54eb570b49fa2d5c905ddf32/portrait.png', kind: 'Item'}
+ {slug: 'north-mounted-camera-facing-east-west', name: 'north mounted camera facing east-west', original: '56f175f2f3ae4cc900a0c5fc', portraitURL: '/file/db/thang.type/56f175f2f3ae4cc900a0c5fc/portrait.png', kind: 'Doodad'}
+ {slug: 'north-mounted-camera-facing-north', name: 'north mounted camera facing north', original: '56f173384852efd20059948a', portraitURL: '/file/db/thang.type/56f173384852efd20059948a/portrait.png', kind: 'Doodad'}
+ {slug: 'north-mounted-camera-facing-south', name: 'north mounted camera facing south', original: '56f17562f3ae4cc900a0c57a', portraitURL: '/file/db/thang.type/56f17562f3ae4cc900a0c57a/portrait.png', kind: 'Doodad'}
+ {slug: 'oak-crossbow', name: 'Oak Crossbow', original: '544d80928494308424f564cb', portraitURL: '/file/db/thang.type/544d80928494308424f564cb/portrait.png', kind: 'Item'}
+ {slug: 'oak-sphere-staff', name: 'Oak Sphere Staff', original: '544d88b78494308424f5650d', portraitURL: '/file/db/thang.type/544d88b78494308424f5650d/portrait.png', kind: 'Item'}
+ {slug: 'oak-wand', name: 'Oak Wand', original: '544d87d18494308424f564fd', portraitURL: '/file/db/thang.type/544d87d18494308424f564fd/portrait.png', kind: 'Item'}
+ {slug: 'oasis-1', name: 'Oasis 1', original: '544d79678494308424f56480', portraitURL: '/file/db/thang.type/544d79678494308424f56480/portrait.png', kind: 'Doodad'}
+ {slug: 'oasis-2', name: 'Oasis 2', original: '544d71198494308424f5647c', portraitURL: '/file/db/thang.type/544d71198494308424f5647c/portrait.png', kind: 'Doodad'}
+ {slug: 'oasis-3', name: 'Oasis 3', original: '5435d22f7b554def1f99c49a', portraitURL: '/file/db/thang.type/5435d22f7b554def1f99c49a/portrait.png', kind: 'Doodad'}
+ {slug: 'obsidian-breastplate', name: 'Obsidian Breastplate', original: '546ab11b3777d6186329286a', portraitURL: '/file/db/thang.type/546ab11b3777d6186329286a/portrait.png', kind: 'Item'}
+ {slug: 'obsidian-helmet', name: 'Obsidian Helmet', original: '546d39989df4a17d0d449a13', portraitURL: '/file/db/thang.type/546d39989df4a17d0d449a13/portrait.png', kind: 'Item'}
+ {slug: 'obsidian-shield', name: 'Obsidian Shield', original: '54eaba502b7506e891ca7216', portraitURL: '/file/db/thang.type/54eaba502b7506e891ca7216/portrait.png', kind: 'Item'}
+ {slug: 'obsidian-staff', name: 'Obsidian Staff', original: '54eab4b92b7506e891ca71ea', portraitURL: '/file/db/thang.type/54eab4b92b7506e891ca71ea/portrait.png', kind: 'Item'}
+ {slug: 'obstacle', name: 'Obstacle', original: '52bcc10d1f766a891c000001', portraitURL: '/file/db/thang.type/52bcc10d1f766a891c000001/portrait.png', kind: 'Misc'}
+ {slug: 'office-chair', name: 'office-chair', original: '56b25d8cc9d8ed21008354b8', portraitURL: '/file/db/thang.type/56b25d8cc9d8ed21008354b8/portrait.png', kind: 'Doodad'}
+ {slug: 'office-desk', name: 'Office Desk', original: '56b26c487168802600d26218', portraitURL: '/file/db/thang.type/56b26c487168802600d26218/portrait.png', kind: 'Doodad'}
+ {slug: 'office-door', name: 'Office Door', original: '56ba3366131fde2a000b84db', portraitURL: '/file/db/thang.type/56ba3366131fde2a000b84db/portrait.png', kind: 'Doodad'}
+ {slug: 'office-filing-cabinet', name: 'Office Filing Cabinet', original: '56b267eec2958a26005fbb58', portraitURL: '/file/db/thang.type/56b267eec2958a26005fbb58/portrait.png', kind: 'Doodad'}
+ {slug: 'office-filing-cabinet-2', name: 'Office Filing Cabinet 2', original: '56b268dd7168802600d25f3d', portraitURL: '/file/db/thang.type/56b268dd7168802600d25f3d/portrait.png', kind: 'Doodad'}
+ {slug: 'office-floor', name: 'Office Floor', original: '56b26e39bb550b26003adef0', portraitURL: '/file/db/thang.type/56b26e39bb550b26003adef0/portrait.png', kind: 'Floor'}
+ {slug: 'office-wall', name: 'office-wall', original: '56abc26c26c92a26005b3745', portraitURL: '/file/db/thang.type/56abc26c26c92a26005b3745/portrait.png', kind: 'Wall'}
+ {slug: 'ogre-barracks', name: 'Ogre Barracks', original: '530d11faa8583eb90a2fc76f', portraitURL: '/file/db/thang.type/530d11faa8583eb90a2fc76f/portrait.png', kind: 'Unit'}
+ {slug: 'ogre-fence', name: 'Ogre Fence', original: '5456b5c5d5ada30000525609', portraitURL: '/file/db/thang.type/5456b5c5d5ada30000525609/portrait.png', kind: 'Doodad'}
+ {slug: 'ogre-fence-2', name: 'Ogre Fence 2', original: '5456b631d5ada3000052560b', portraitURL: '/file/db/thang.type/5456b631d5ada3000052560b/portrait.png', kind: 'Doodad'}
+ {slug: 'ogre-headhunter-hero', name: 'Ogre Headhunter Hero', original: '5670779dfb9b702400cf6987', portraitURL: '/file/db/thang.type/5670779dfb9b702400cf6987/portrait.png', kind: 'Unit'}
+ {slug: 'ogre-peon-f', name: 'Ogre Peon F', original: '53765709a7b5ab3805f15512', portraitURL: '/file/db/thang.type/53765709a7b5ab3805f15512/portrait.png', kind: 'Unit'}
+ {slug: 'ogre-peon-m', name: 'Ogre Peon M', original: '53793734f883583805e356e2', portraitURL: '/file/db/thang.type/53793734f883583805e356e2/portrait.png', kind: 'Unit'}
+ {slug: 'ogre-scout-f', name: 'Ogre Scout F', original: '54909436b30e9eb7027fe21c', portraitURL: '/file/db/thang.type/54909436b30e9eb7027fe21c/portrait.png', kind: 'Unit'}
+ {slug: 'ogre-scout-m', name: 'Ogre Scout M', original: '54908ce5b30e9eb7027fe201', portraitURL: '/file/db/thang.type/54908ce5b30e9eb7027fe201/portrait.png', kind: 'Unit'}
+ {slug: 'ogre-tent', name: 'Ogre Tent', original: '5456b49dd5ada30000525607', portraitURL: '/file/db/thang.type/5456b49dd5ada30000525607/portrait.png', kind: 'Doodad'}
+ {slug: 'ogre-tower', name: 'ogre tower', original: '578686459fabcb1f0087d064', portraitURL: '/file/db/thang.type/578686459fabcb1f0087d064/portrait.png', kind: 'Doodad'}
+ {slug: 'ogre-tower-with-desert-rocks', name: 'ogre tower with desert rocks', original: '572d465dab2d38ad00a1c918', portraitURL: '/file/db/thang.type/572d465dab2d38ad00a1c918/portrait.png', kind: 'Doodad'}
+ {slug: 'ogre-towers-with-trees', name: 'ogre towers with trees', original: '572d47eee24ce2fb0025c6f3', portraitURL: '/file/db/thang.type/572d47eee24ce2fb0025c6f3/portrait.png', kind: 'Doodad'}
+ {slug: 'ogre-treasure-chest', name: 'Ogre Treasure Chest', original: '540e16d6821af8000097dc55', portraitURL: '/file/db/thang.type/540e16d6821af8000097dc55/portrait.png', kind: 'Doodad'}
+ {slug: 'ogre-wall', name: 'ogre wall', original: '5786834a2437842400f4009c', portraitURL: '/file/db/thang.type/5786834a2437842400f4009c/portrait.png', kind: 'Doodad'}
+ {slug: 'ogre-witch-hero', name: 'Ogre Witch Hero', original: '5638f6c4ef9d6464094a559d', portraitURL: '/file/db/thang.type/5638f6c4ef9d6464094a559d/portrait.png', kind: 'Unit'}
+ {slug: 'old-selection', name: 'Old Selection', original: '52aa5f7520fccb0000000002', portraitURL: '/file/db/thang.type/52aa5f7520fccb0000000002/portrait.png', kind: 'Mark'}
+ {slug: 'order-of-the-paladin', name: 'Order of the Paladin', original: '54eb55af49fa2d5c905ddf22', portraitURL: '/file/db/thang.type/54eb55af49fa2d5c905ddf22/portrait.png', kind: 'Item'}
+ {slug: 'overseer', name: 'Overseer', original: '56e75e0b67a0142000a12699', portraitURL: '/file/db/thang.type/56e75e0b67a0142000a12699/portrait.png', kind: 'Unit'}
+ {slug: 'painted-steel-breastplate', name: 'Painted Steel Breastplate', original: '546ab0dd3777d61863292866', portraitURL: '/file/db/thang.type/546ab0dd3777d61863292866/portrait.png', kind: 'Item'}
+ {slug: 'painted-steel-helmet', name: 'Painted Steel Helmet', original: '546d39589df4a17d0d449a0f', portraitURL: '/file/db/thang.type/546d39589df4a17d0d449a0f/portrait.png', kind: 'Item'}
+ {slug: 'painted-steel-shield', name: 'Painted Steel Shield', original: '544d7c5b8494308424f5649b', portraitURL: '/file/db/thang.type/544d7c5b8494308424f5649b/portrait.png', kind: 'Item'}
+ {slug: 'palisade', name: 'Palisade', original: '546e24bd9df4a17d0d449bc9', portraitURL: '/file/db/thang.type/546e24bd9df4a17d0d449bc9/portrait.png', kind: 'Doodad'}
+ {slug: 'paralyze', name: 'Paralyze', original: '53024e6b222f73867774d773', portraitURL: '/file/db/thang.type/53024e6b222f73867774d773/portrait.png', kind: 'Mark'}
+ {slug: 'pedestal', name: 'Pedestal', original: '542ae4750048dcb95727a1e6', portraitURL: '/file/db/thang.type/542ae4750048dcb95727a1e6/portrait.png', kind: 'Doodad'}
+ {slug: 'phoenixfire', name: 'Phoenixfire', original: '54ea8b602b7506e891ca718d', portraitURL: '/file/db/thang.type/54ea8b602b7506e891ca718d/portrait.png', kind: 'Item'}
+ {slug: 'plasma-ball', name: 'Plasma Ball', original: '5589fe594bed1b6c2a2cab6b', portraitURL: '/file/db/thang.type/5589fe594bed1b6c2a2cab6b/portrait.png', kind: 'Missile'}
+ {slug: 'poison', name: 'Poison', original: '53024020222f73867774d619', portraitURL: '/file/db/thang.type/53024020222f73867774d619/portrait.png', kind: 'Mark'}
+ {slug: 'poisoned-throwing-shard-missile', name: 'Poisoned Throwing Shard Missile', original: '544d97088494308424f56539', portraitURL: '/file/db/thang.type/544d97088494308424f56539/portrait.png', kind: 'Missile'}
+ {slug: 'polar-bear-cub-pet', name: 'Polar Bear Cub pet', original: '57588d4b87b06e1f00ded849', portraitURL: '/file/db/thang.type/57588d4b87b06e1f00ded849/portrait.png', kind: 'Unit'}
+ {slug: 'polished-agate-sense-stone', name: 'Polished Agate Sense Stone', original: '54693274a2b1f53ce79443c9', portraitURL: '/file/db/thang.type/54693274a2b1f53ce79443c9/portrait.png', kind: 'Item'}
+ {slug: 'polished-bronze-breastplate', name: 'Polished Bronze Breastplate', original: '545d3f0b2d03e700001b5a5d', portraitURL: '/file/db/thang.type/545d3f0b2d03e700001b5a5d/portrait.png', kind: 'Item'}
+ {slug: 'polished-bronze-helmet', name: 'Polished Bronze Helmet', original: '546d38779df4a17d0d449a03', portraitURL: '/file/db/thang.type/546d38779df4a17d0d449a03/portrait.png', kind: 'Item'}
+ {slug: 'polished-bronze-shield', name: 'Polished Bronze Shield', original: '544d7a888494308424f56487', portraitURL: '/file/db/thang.type/544d7a888494308424f56487/portrait.png', kind: 'Item'}
+ {slug: 'polished-emerald-sense-stone', name: 'Polished Emerald Sense Stone', original: '546933dda2b1f53ce79443d9', portraitURL: '/file/db/thang.type/546933dda2b1f53ce79443d9/portrait.png', kind: 'Item'}
+ {slug: 'polished-sense-stone', name: 'Polished Sense Stone', original: '53e215a253457600003e3eaf', portraitURL: '/file/db/thang.type/53e215a253457600003e3eaf/portrait.png', kind: 'Item'}
+ {slug: 'polished-steel-scale-chainmail-coif', name: 'Polished Steel Scale Chainmail Coif', original: '546d46889df4a17d0d449a5f', portraitURL: '/file/db/thang.type/546d46889df4a17d0d449a5f/portrait.png', kind: 'Item'}
+ {slug: 'polished-steel-scale-chainmail-tunic', name: 'Polished Steel Scale Chainmail Tunic', original: '546d3c3f9df4a17d0d449a37', portraitURL: '/file/db/thang.type/546d3c3f9df4a17d0d449a37/portrait.png', kind: 'Item'}
+ {slug: 'pot-1', name: 'Pot 1', original: '54ef882f83b08b7d054b6d49', portraitURL: '/file/db/thang.type/54ef882f83b08b7d054b6d49/portrait.png', kind: 'Doodad'}
+ {slug: 'pot-2', name: 'Pot 2', original: '54ef89dc4bb4788505d21234', portraitURL: '/file/db/thang.type/54ef89dc4bb4788505d21234/portrait.png', kind: 'Doodad'}
+ {slug: 'pot-3', name: 'Pot 3', original: '54ef8b1f305d7e790557d5d5', portraitURL: '/file/db/thang.type/54ef8b1f305d7e790557d5d5/portrait.png', kind: 'Doodad'}
+ {slug: 'pot-4', name: 'Pot 4', original: '54ef8becace2147e05868483', portraitURL: '/file/db/thang.type/54ef8becace2147e05868483/portrait.png', kind: 'Doodad'}
+ {slug: 'potion-belt', name: 'Potion Belt', original: '54694ac4a2b1f53ce794443d', portraitURL: '/file/db/thang.type/54694ac4a2b1f53ce794443d/portrait.png', kind: 'Item'}
+ {slug: 'potted-tree', name: 'Potted Tree', original: '56b2c8baf2ea182100d8ce78', portraitURL: '/file/db/thang.type/56b2c8baf2ea182100d8ce78/portrait.png', kind: 'Doodad'}
+ {slug: 'powder-charge', name: 'Powder Charge', original: '5462952cf44055a4b5e73599', portraitURL: '/file/db/thang.type/5462952cf44055a4b5e73599/portrait.png', kind: 'Item'}
+ {slug: 'powder-charge-missile', name: 'Powder Charge Missile', original: '544d99328494308424f56540', portraitURL: '/file/db/thang.type/544d99328494308424f56540/portrait.png', kind: 'Missile'}
+ {slug: 'power-up', name: 'Power Up', original: '55c64140ef141c65665beb6b', portraitURL: '/file/db/thang.type/55c64140ef141c65665beb6b/portrait.png', kind: 'Mark'}
+ {slug: 'power-up-2', name: 'Power Up 2', original: '55c6419fef141c65665beb6f', portraitURL: '/file/db/thang.type/55c6419fef141c65665beb6f/portrait.png', kind: 'Mark'}
+ {slug: 'precision-rifle', name: 'Precision Rifle', original: '54eaaecc2b7506e891ca71d9', portraitURL: '/file/db/thang.type/54eaaecc2b7506e891ca71d9/portrait.png', kind: 'Item'}
+ {slug: 'programmaticon-i', name: 'Programmaticon I', original: '53e4108204c00d4607a89f78', portraitURL: '/file/db/thang.type/53e4108204c00d4607a89f78/portrait.png', kind: 'Item'}
+ {slug: 'programmaticon-ii', name: 'Programmaticon II', original: '546e25d99df4a17d0d449be1', portraitURL: '/file/db/thang.type/546e25d99df4a17d0d449be1/portrait.png', kind: 'Item'}
+ {slug: 'programmaticon-iii', name: 'Programmaticon III', original: '546e266e9df4a17d0d449be5', portraitURL: '/file/db/thang.type/546e266e9df4a17d0d449be5/portrait.png', kind: 'Item'}
+ {slug: 'programmaticon-iv', name: 'Programmaticon IV', original: '55240951f76d6ee949f66512', portraitURL: '/file/db/thang.type/55240951f76d6ee949f66512/portrait.png', kind: 'Item'}
+ {slug: 'programmaticon-v', name: 'Programmaticon V', original: '557871261ff17fef5abee3ee', portraitURL: '/file/db/thang.type/557871261ff17fef5abee3ee/portrait.png', kind: 'Item'}
+ {slug: 'pugicorn-pet', name: 'Pugicorn Pet', original: '577d5edcab818b210046b73c', portraitURL: '/file/db/thang.type/577d5edcab818b210046b73c/portrait.png', kind: 'Unit'}
+ {slug: 'pushcart', name: 'Pushcart', original: '54f119a6d2969f8405ef539f', portraitURL: '/file/db/thang.type/54f119a6d2969f8405ef539f/portrait.png', kind: 'Doodad'}
+ {slug: 'quartz-sense-stone', name: 'Quartz Sense Stone', original: '54693240a2b1f53ce79443c5', portraitURL: '/file/db/thang.type/54693240a2b1f53ce79443c5/portrait.png', kind: 'Item'}
+ {slug: 'ragged-silk-hat', name: 'Ragged Silk Hat', original: '546d4ba19df4a17d0d449aaf', portraitURL: '/file/db/thang.type/546d4ba19df4a17d0d449aaf/portrait.png', kind: 'Item'}
+ {slug: 'railgun', name: 'Railgun', original: '54ea8ea52b7506e891ca7191', portraitURL: '/file/db/thang.type/54ea8ea52b7506e891ca7191/portrait.png', kind: 'Item'}
+ {slug: 'rapidfire-rifle', name: 'Rapidfire Rifle', original: '54eaae422b7506e891ca71d5', portraitURL: '/file/db/thang.type/54eaae422b7506e891ca71d5/portrait.png', kind: 'Item'}
+ {slug: 'rat', name: 'Rat', original: '55c11b70c87e47c60604f974', portraitURL: '/file/db/thang.type/55c11b70c87e47c60604f974/portrait.png', kind: 'Doodad'}
+ {slug: 'razor-ring', name: 'Razor Ring', original: '54c97c9bdef3ad363ff998b7', portraitURL: '/file/db/thang.type/54c97c9bdef3ad363ff998b7/portrait.png', kind: 'Missile'}
+ {slug: 'razordisc-missile', name: 'Razordisc Missile', original: '5318d3e56ad8999d34bdf338', portraitURL: '/file/db/thang.type/5318d3e56ad8999d34bdf338/portrait.png', kind: 'Missile'}
+ {slug: 'rectangle', name: 'Rectangle', original: '568d915e1717e2f90e9a1250', portraitURL: '/file/db/thang.type/568d915e1717e2f90e9a1250/portrait.png', kind: 'Misc'}
+ {slug: 'red-button', name: 'Red Button', original: '56d102c0441ddd2f002ba760', portraitURL: '/file/db/thang.type/56d102c0441ddd2f002ba760/portrait.png', kind: 'Doodad'}
+ {slug: 'regen', name: 'Regen', original: '53024f8b27471514685d53e1', portraitURL: '/file/db/thang.type/53024f8b27471514685d53e1/portrait.png', kind: 'Mark'}
+ {slug: 'reindeer', name: 'Reindeer', original: '54e95a88f54ef5794f354f3d', portraitURL: '/file/db/thang.type/54e95a88f54ef5794f354f3d/portrait.png', kind: 'Doodad'}
+ {slug: 'reinforced-boots', name: 'Reinforced Boots', original: '546d4d259df4a17d0d449ac5', portraitURL: '/file/db/thang.type/546d4d259df4a17d0d449ac5/portrait.png', kind: 'Item'}
+ {slug: 'reinforced-crossbow', name: 'Reinforced Crossbow', original: '54eaacdd2b7506e891ca71cd', portraitURL: '/file/db/thang.type/54eaacdd2b7506e891ca71cd/portrait.png', kind: 'Item'}
+ {slug: 'reinforced-iron-chainmail-coif', name: 'Reinforced Iron Chainmail Coif', original: '546d46099df4a17d0d449a57', portraitURL: '/file/db/thang.type/546d46099df4a17d0d449a57/portrait.png', kind: 'Item'}
+ {slug: 'reinforced-iron-chainmail-tunic', name: 'Reinforced Iron Chainmail Tunic', original: '546d3bbb9df4a17d0d449a2f', portraitURL: '/file/db/thang.type/546d3bbb9df4a17d0d449a2f/portrait.png', kind: 'Item'}
+ {slug: 'repair', name: 'Repair', original: '52bcc4591f766a891c000003', portraitURL: '/file/db/thang.type/52bcc4591f766a891c000003/portrait.png', kind: 'Mark'}
+ {slug: 'ring-of-developer-experimentation', name: 'Ring of Developer Experimentation', original: '54bac99bacbf5aea089da177', portraitURL: '/file/db/thang.type/54bac99bacbf5aea089da177/portrait.png', kind: 'Item'}
+ {slug: 'ring-of-earth', name: 'Ring of Earth', original: '5441c35c4e9aeb727cc9711d', portraitURL: '/file/db/thang.type/5441c35c4e9aeb727cc9711d/portrait.png', kind: 'Item'}
+ {slug: 'ring-of-fire', name: 'Ring of Fire', original: '54692ea2a2b1f53ce79443ab', portraitURL: '/file/db/thang.type/54692ea2a2b1f53ce79443ab/portrait.png', kind: 'Item'}
+ {slug: 'ring-of-flowers', name: 'Ring of Flowers', original: '5523224b0676ecb7d5c89319', portraitURL: '/file/db/thang.type/5523224b0676ecb7d5c89319/portrait.png', kind: 'Item'}
+ {slug: 'ring-of-ice', name: 'Ring of Ice', original: '54692ed3a2b1f53ce79443af', portraitURL: '/file/db/thang.type/54692ed3a2b1f53ce79443af/portrait.png', kind: 'Item'}
+ {slug: 'ring-of-speed', name: 'Ring of Speed', original: '54692d2aa2b1f53ce794438f', portraitURL: '/file/db/thang.type/54692d2aa2b1f53ce794438f/portrait.png', kind: 'Item'}
+ {slug: 'riveted-dragonscale-chainmail-coif', name: 'Riveted Dragonscale Chainmail Coif', original: '546d47c09df4a17d0d449a6f', portraitURL: '/file/db/thang.type/546d47c09df4a17d0d449a6f/portrait.png', kind: 'Item'}
+ {slug: 'riveted-dragonscale-chainmail-tunic', name: 'Riveted Dragonscale Chainmail Tunic', original: '546d3d549df4a17d0d449a47', portraitURL: '/file/db/thang.type/546d3d549df4a17d0d449a47/portrait.png', kind: 'Item'}
+ {slug: 'robe-of-the-magi', name: 'Robe of the Magi', original: '54ea3ec22b7506e891ca7126', portraitURL: '/file/db/thang.type/54ea3ec22b7506e891ca7126/portrait.png', kind: 'Item'}
+ {slug: 'robobomb', name: 'Robobomb', original: '55b7fb22a337d9b0ea024bb4', portraitURL: '/file/db/thang.type/55b7fb22a337d9b0ea024bb4/portrait.png', kind: 'Unit'}
+ {slug: 'robot-walker', name: 'Robot Walker', original: '5301696ad82649ec2c0c9b0d', portraitURL: '/file/db/thang.type/5301696ad82649ec2c0c9b0d/portrait.png', kind: 'Unit'}
+ {slug: 'rock-1', name: 'Rock 1', original: '52afcc1fc5b1813ec2000010', portraitURL: '/file/db/thang.type/52afcc1fc5b1813ec2000010/portrait.png', kind: 'Doodad'}
+ {slug: 'rock-2', name: 'Rock 2', original: '52afcce4c5b1813ec2000012', portraitURL: '/file/db/thang.type/52afcce4c5b1813ec2000012/portrait.png', kind: 'Doodad'}
+ {slug: 'rock-3', name: 'Rock 3', original: '52afcd43c5b1813ec2000014', portraitURL: '/file/db/thang.type/52afcd43c5b1813ec2000014/portrait.png', kind: 'Doodad'}
+ {slug: 'rock-4', name: 'Rock 4', original: '52afcd7bc5b1813ec2000016', portraitURL: '/file/db/thang.type/52afcd7bc5b1813ec2000016/portrait.png', kind: 'Doodad'}
+ {slug: 'rock-5', name: 'Rock 5', original: '52afcdc7c5b1813ec2000018', portraitURL: '/file/db/thang.type/52afcdc7c5b1813ec2000018/portrait.png', kind: 'Doodad'}
+ {slug: 'rock-6', name: 'Rock 6', original: '54e95916f54ef5794f354f2d', portraitURL: '/file/db/thang.type/54e95916f54ef5794f354f2d/portrait.png', kind: 'Doodad'}
+ {slug: 'rock-7', name: 'Rock 7', original: '54e959d6f54ef5794f354f31', portraitURL: '/file/db/thang.type/54e959d6f54ef5794f354f31/portrait.png', kind: 'Doodad'}
+ {slug: 'rock-8', name: 'Rock 8', original: '54e95a10f54ef5794f354f35', portraitURL: '/file/db/thang.type/54e95a10f54ef5794f354f35/portrait.png', kind: 'Doodad'}
+ {slug: 'rock-cluster-1', name: 'Rock Cluster 1', original: '52afcb47c5b1813ec200000a', portraitURL: '/file/db/thang.type/52afcb47c5b1813ec200000a/portrait.png', kind: 'Doodad'}
+ {slug: 'rock-cluster-2', name: 'Rock Cluster 2', original: '52afcb98c5b1813ec200000c', portraitURL: '/file/db/thang.type/52afcb98c5b1813ec200000c/portrait.png', kind: 'Doodad'}
+ {slug: 'rock-cluster-3', name: 'Rock Cluster 3', original: '52afcbe0c5b1813ec200000e', portraitURL: '/file/db/thang.type/52afcbe0c5b1813ec200000e/portrait.png', kind: 'Doodad'}
+ {slug: 'rock-field-1', name: 'Rock Field 1', original: '54e95753f54ef5794f354f21', portraitURL: '/file/db/thang.type/54e95753f54ef5794f354f21/portrait.png', kind: 'Doodad'}
+ {slug: 'rock-field-2', name: 'Rock Field 2', original: '54e95861f54ef5794f354f25', portraitURL: '/file/db/thang.type/54e95861f54ef5794f354f25/portrait.png', kind: 'Doodad'}
+ {slug: 'rock-field-3', name: 'Rock Field 3', original: '54e958aaf54ef5794f354f29', portraitURL: '/file/db/thang.type/54e958aaf54ef5794f354f29/portrait.png', kind: 'Doodad'}
+ {slug: 'root', name: 'Root', original: '55c640feef141c65665beb67', portraitURL: '/file/db/thang.type/55c640feef141c65665beb67/portrait.png', kind: 'Mark'}
+ {slug: 'rough-sense-stone', name: 'Rough Sense Stone', original: '54693140a2b1f53ce79443bc', portraitURL: '/file/db/thang.type/54693140a2b1f53ce79443bc/portrait.png', kind: 'Item'}
+ {slug: 'roughedge', name: 'Roughedge', original: '544d7d918494308424f564a7', portraitURL: '/file/db/thang.type/544d7d918494308424f564a7/portrait.png', kind: 'Item'}
+ {slug: 'rs-demo', name: 'RS Demo', original: '56ce48892438c720001e3ca3', portraitURL: '/file/db/thang.type/56ce48892438c720001e3ca3/portrait.png', kind: 'undefined'}
+ {slug: 'runesword', name: 'Runesword', original: '54eaa9622b7506e891ca71b1', portraitURL: '/file/db/thang.type/54eaa9622b7506e891ca71b1/portrait.png', kind: 'Item'}
+ {slug: 'rusted-iron-breastplate', name: 'Rusted Iron Breastplate', original: '545d3fe42d03e700001b5a5f', portraitURL: '/file/db/thang.type/545d3fe42d03e700001b5a5f/portrait.png', kind: 'Item'}
+ {slug: 'rusted-iron-helmet', name: 'Rusted Iron Helmet', original: '546d38d09df4a17d0d449a07', portraitURL: '/file/db/thang.type/546d38d09df4a17d0d449a07/portrait.png', kind: 'Item'}
+ {slug: 'rusted-steel-scale-chainmail-coif', name: 'Rusted Steel Scale Chainmail Coif', original: '546d46419df4a17d0d449a5b', portraitURL: '/file/db/thang.type/546d46419df4a17d0d449a5b/portrait.png', kind: 'Item'}
+ {slug: 'rusted-steel-scale-chainmail-tunic', name: 'Rusted Steel Scale Chainmail Tunic', original: '546d3bf99df4a17d0d449a33', portraitURL: '/file/db/thang.type/546d3bf99df4a17d0d449a33/portrait.png', kind: 'Item'}
+ {slug: 'sand-01', name: 'Sand 01', original: '5484df79d7b7b862291456af', portraitURL: '/file/db/thang.type/5484df79d7b7b862291456af/portrait.png', kind: 'Floor'}
+ {slug: 'sand-02', name: 'Sand 02', original: '5484e7c5d7b7b862291456b3', portraitURL: '/file/db/thang.type/5484e7c5d7b7b862291456b3/portrait.png', kind: 'Floor'}
+ {slug: 'sand-03', name: 'Sand 03', original: '5484e81bd7b7b862291456b7', portraitURL: '/file/db/thang.type/5484e81bd7b7b862291456b7/portrait.png', kind: 'Floor'}
+ {slug: 'sand-04', name: 'Sand 04', original: '5484e857d7b7b862291456bb', portraitURL: '/file/db/thang.type/5484e857d7b7b862291456bb/portrait.png', kind: 'Floor'}
+ {slug: 'sand-05', name: 'Sand 05', original: '5484e89cd7b7b862291456bf', portraitURL: '/file/db/thang.type/5484e89cd7b7b862291456bf/portrait.png', kind: 'Floor'}
+ {slug: 'sand-06', name: 'Sand 06', original: '5484e8ddd7b7b862291456c3', portraitURL: '/file/db/thang.type/5484e8ddd7b7b862291456c3/portrait.png', kind: 'Floor'}
+ {slug: 'sand-yak', name: 'Sand Yak', original: '5480b2251bf0b10000711c51', portraitURL: '/file/db/thang.type/5480b2251bf0b10000711c51/portrait.png', kind: 'Unit'}
+ {slug: 'sapphire-sense-stone', name: 'Sapphire Sense Stone', original: '54693363a2b1f53ce79443d1', portraitURL: '/file/db/thang.type/54693363a2b1f53ce79443d1/portrait.png', kind: 'Item'}
+ {slug: 'sarcophagus', name: 'sarcophagus', original: '572d5a2d3ff46db2000a381b', portraitURL: '/file/db/thang.type/572d5a2d3ff46db2000a381b/portrait.png', kind: 'Doodad'}
+ {slug: 'scaled-gloves', name: 'Scaled Gloves', original: '5469496ca2b1f53ce794442d', portraitURL: '/file/db/thang.type/5469496ca2b1f53ce794442d/portrait.png', kind: 'Item'}
+ {slug: 'school-locker', name: 'School locker', original: '56eb14804eb67a25009be23e', portraitURL: '/file/db/thang.type/56eb14804eb67a25009be23e/portrait.png', kind: 'Doodad'}
+ {slug: 'scoreboard', name: 'Scoreboard', original: '56de0ff26f9cc02400831e06', portraitURL: '/file/db/thang.type/56de0ff26f9cc02400831e06/portrait.png', kind: 'Doodad'}
+ {slug: 'scorpion', name: 'Scorpion', original: '548cf5340f559d0000be7e5b', portraitURL: '/file/db/thang.type/548cf5340f559d0000be7e5b/portrait.png', kind: 'Doodad'}
+ {slug: 'selection', name: 'Selection', original: '546e23d49df4a17d0d449bb5', portraitURL: '/file/db/thang.type/546e23d49df4a17d0d449bb5/portrait.png', kind: 'Misc'}
+ {slug: 'shadow-guard-background', name: 'Shadow Guard Background', original: '55bfc4c950cac5d58def9a67', portraitURL: '/file/db/thang.type/55bfc4c950cac5d58def9a67/portrait.png', kind: 'Floor'}
+ {slug: 'shadowless-bird', name: 'Shadowless Bird', original: '55079c55cea461db22519e9d', portraitURL: '/file/db/thang.type/55079c55cea461db22519e9d/portrait.png', kind: 'Doodad'}
+ {slug: 'shadowless-cloud-1', name: 'Shadowless Cloud 1', original: '53e2df9cd12e873205b6bce8', portraitURL: '/file/db/thang.type/53e2df9cd12e873205b6bce8/portrait.png', kind: 'Doodad'}
+ {slug: 'shadowless-cloud-2', name: 'Shadowless Cloud 2', original: '53e2e0176c59f5340504102f', portraitURL: '/file/db/thang.type/53e2e0176c59f5340504102f/portrait.png', kind: 'Doodad'}
+ {slug: 'shadowless-cloud-3', name: 'Shadowless Cloud 3', original: '53e2e08eae44ec37059f2148', portraitURL: '/file/db/thang.type/53e2e08eae44ec37059f2148/portrait.png', kind: 'Doodad'}
+ {slug: 'sharpened-sword', name: 'Sharpened Sword', original: '544d7deb8494308424f564ab', portraitURL: '/file/db/thang.type/544d7deb8494308424f564ab/portrait.png', kind: 'Item'}
+ {slug: 'sharpsong', name: 'Sharpsong', original: '544d95c78494308424f56527', portraitURL: '/file/db/thang.type/544d95c78494308424f56527/portrait.png', kind: 'Item'}
+ {slug: 'sharpsong-missile', name: 'Sharpsong Missile', original: '544d98368494308424f5653e', portraitURL: '/file/db/thang.type/544d98368494308424f5653e/portrait.png', kind: 'Missile'}
+ {slug: 'shell', name: 'Shell', original: '52ba2c6c981fbb7e48000093', portraitURL: '/file/db/thang.type/52ba2c6c981fbb7e48000093/portrait.png', kind: 'Missile'}
+ {slug: 'shield', name: 'Shield', original: '573fa531d0bee72000a4255f', portraitURL: '/file/db/thang.type/573fa531d0bee72000a4255f/portrait.png', kind: 'Mark'}
+ {slug: 'short-sword', name: 'Short Sword', original: '544d7f1a8494308424f564b7', portraitURL: '/file/db/thang.type/544d7f1a8494308424f564b7/portrait.png', kind: 'Item'}
+ {slug: 'shrub-1', name: 'Shrub 1', original: '52b0a113ccbc671372000017', portraitURL: '/file/db/thang.type/52b0a113ccbc671372000017/portrait.png', kind: 'Doodad'}
+ {slug: 'shrub-2', name: 'Shrub 2', original: '52b0a15accbc671372000019', portraitURL: '/file/db/thang.type/52b0a15accbc671372000019/portrait.png', kind: 'Doodad'}
+ {slug: 'shrub-3', name: 'Shrub 3', original: '52b0a1a3ccbc67137200001b', portraitURL: '/file/db/thang.type/52b0a1a3ccbc67137200001b/portrait.png', kind: 'Doodad'}
+ {slug: 'sign', name: 'Sign', original: '5435cbe77b554def1f99c491', portraitURL: '/file/db/thang.type/5435cbe77b554def1f99c491/portrait.png', kind: 'Doodad'}
+ {slug: 'silver-coin', name: 'Silver Coin', original: '535ef1f64f10444d08486b61', portraitURL: '/file/db/thang.type/535ef1f64f10444d08486b61/portrait.png', kind: 'Misc'}
+ {slug: 'simple-boots', name: 'Simple Boots', original: '53e237bf53457600003e3f05', portraitURL: '/file/db/thang.type/53e237bf53457600003e3f05/portrait.png', kind: 'Item'}
+ {slug: 'simple-katana', name: 'Simple Katana', original: '544d7ed58494308424f564b3', portraitURL: '/file/db/thang.type/544d7ed58494308424f564b3/portrait.png', kind: 'Item'}
+ {slug: 'simple-rifle', name: 'Simple Rifle', original: '544d70a18494308424f5647a', portraitURL: '/file/db/thang.type/544d70a18494308424f5647a/portrait.png', kind: 'Item'}
+ {slug: 'simple-sword', name: 'Simple Sword', original: '53e218d853457600003e3ebe', portraitURL: '/file/db/thang.type/53e218d853457600003e3ebe/portrait.png', kind: 'Item'}
+ {slug: 'simple-wand', name: 'Simple Wand', original: '544d874f8494308424f564f5', portraitURL: '/file/db/thang.type/544d874f8494308424f564f5/portrait.png', kind: 'Item'}
+ {slug: 'simple-wristwatch', name: 'Simple Wristwatch', original: '54693797a2b1f53ce79443e9', portraitURL: '/file/db/thang.type/54693797a2b1f53ce79443e9/portrait.png', kind: 'Item'}
+ {slug: 'skeleton-bits-1', name: 'Skeleton Bits 1', original: '54ef85bdc1f3bd7c0593d125', portraitURL: '/file/db/thang.type/54ef85bdc1f3bd7c0593d125/portrait.png', kind: 'Doodad'}
+ {slug: 'skeleton-bits-2', name: 'Skeleton Bits 2', original: '54ef874370ff9c8005e1eb0d', portraitURL: '/file/db/thang.type/54ef874370ff9c8005e1eb0d/portrait.png', kind: 'Doodad'}
+ {slug: 'sky-span-background-1', name: 'Sky Span Background 1', original: '53e3f096ae44ec37059f92d8', portraitURL: '/file/db/thang.type/53e3f096ae44ec37059f92d8/portrait.png', kind: 'Floor'}
+ {slug: 'sky-span-background-2', name: 'Sky Span Background 2', original: '53e3f3556c59f5340504359e', portraitURL: '/file/db/thang.type/53e3f3556c59f5340504359e/portrait.png', kind: 'Floor'}
+ {slug: 'sky-span-background-3', name: 'Sky Span Background 3', original: '53e3f500ae44ec37059f9415', portraitURL: '/file/db/thang.type/53e3f500ae44ec37059f9415/portrait.png', kind: 'Doodad'}
+ {slug: 'sky-span-background-4', name: 'Sky Span Background 4', original: '53e3f5dbae44ec37059f944a', portraitURL: '/file/db/thang.type/53e3f5dbae44ec37059f944a/portrait.png', kind: 'Doodad'}
+ {slug: 'sky-span-background-5', name: 'Sky Span Background 5', original: '53e3f646d12e873205b72abd', portraitURL: '/file/db/thang.type/53e3f646d12e873205b72abd/portrait.png', kind: 'Floor'}
+ {slug: 'sky-span-background-6', name: 'Sky Span Background 6', original: '53e3f724d12e873205b72af9', portraitURL: '/file/db/thang.type/53e3f724d12e873205b72af9/portrait.png', kind: 'Floor'}
+ {slug: 'sky-span-background-7', name: 'Sky Span Background 7', original: '53e3f74dae44ec37059f94b3', portraitURL: '/file/db/thang.type/53e3f74dae44ec37059f94b3/portrait.png', kind: 'Doodad'}
+ {slug: 'sleep', name: 'Sleep', original: '5302504b222f73867774d7a1', portraitURL: '/file/db/thang.type/5302504b222f73867774d7a1/portrait.png', kind: 'Mark'}
+ {slug: 'slow', name: 'Slow', original: '5302511327471514685d5405', portraitURL: '/file/db/thang.type/5302511327471514685d5405/portrait.png', kind: 'Mark'}
+ {slug: 'snake', name: 'Snake', original: '548cf57c0f559d0000be7e5f', portraitURL: '/file/db/thang.type/548cf57c0f559d0000be7e5f/portrait.png', kind: 'Doodad'}
+ {slug: 'snake-pillar', name: 'Snake Pillar', original: '54ef8db1223edd8105aff2b9', portraitURL: '/file/db/thang.type/54ef8db1223edd8105aff2b9/portrait.png', kind: 'Doodad'}
+ {slug: 'soft-leather-gloves', name: 'Soft Leather Gloves', original: '546948e9a2b1f53ce7944425', portraitURL: '/file/db/thang.type/546948e9a2b1f53ce7944425/portrait.png', kind: 'Item'}
+ {slug: 'softened-leather-boots', name: 'Softened Leather Boots', original: '546d4d589df4a17d0d449ac9', portraitURL: '/file/db/thang.type/546d4d589df4a17d0d449ac9/portrait.png', kind: 'Item'}
+ {slug: 'sparkbomb', name: 'Sparkbomb', original: '54eb528449fa2d5c905ddf12', portraitURL: '/file/db/thang.type/54eb528449fa2d5c905ddf12/portrait.png', kind: 'Item'}
+ {slug: 'sparkbomb-missile', name: 'Sparkbomb Missile', original: '5535b3bd428ddac5686fcf7a', portraitURL: '/file/db/thang.type/5535b3bd428ddac5686fcf7a/portrait.png', kind: 'Missile'}
+ {slug: 'spear', name: 'Spear', original: '52ba2affd68e4b7c48000030', portraitURL: '/file/db/thang.type/52ba2affd68e4b7c48000030/portrait.png', kind: 'Missile'}
+ {slug: 'spider', name: 'Spider', original: '55c1353bc87e47c60604f997', portraitURL: '/file/db/thang.type/55c1353bc87e47c60604f997/portrait.png', kind: 'Doodad'}
+ {slug: 'spiderweb-1', name: 'Spiderweb 1', original: '54ef7c69223edd8105afc1f4', portraitURL: '/file/db/thang.type/54ef7c69223edd8105afc1f4/portrait.png', kind: 'Doodad'}
+ {slug: 'spiderweb-2', name: 'Spiderweb 2', original: '54ef7d41ace2147e058655a8', portraitURL: '/file/db/thang.type/54ef7d41ace2147e058655a8/portrait.png', kind: 'Doodad'}
+ {slug: 'spiderweb-3', name: 'Spiderweb 3', original: '54ef7ed7b4740779058410c9', portraitURL: '/file/db/thang.type/54ef7ed7b4740779058410c9/portrait.png', kind: 'Doodad'}
+ {slug: 'spiderweb-4', name: 'Spiderweb 4', original: '54ef8938ace2147e05867d6d', portraitURL: '/file/db/thang.type/54ef8938ace2147e05867d6d/portrait.png', kind: 'Doodad'}
+ {slug: 'spike-walls', name: 'Spike Walls', original: '5422f63718adb78d98d265f7', portraitURL: '/file/db/thang.type/5422f63718adb78d98d265f7/portrait.png', kind: 'Doodad'}
+ {slug: 'spiked-ogre-wall', name: 'spiked ogre wall', original: '578682dccca8994b002708eb', portraitURL: '/file/db/thang.type/578682dccca8994b002708eb/portrait.png', kind: 'Doodad'}
+ {slug: 'stalactite-1', name: 'Stalactite 1', original: '55760f0a1e82182d9e688912', portraitURL: '/file/db/thang.type/55760f0a1e82182d9e688912/portrait.png', kind: 'Doodad'}
+ {slug: 'stalactite-2', name: 'Stalactite 2', original: '55760f4a1e82182d9e688916', portraitURL: '/file/db/thang.type/55760f4a1e82182d9e688916/portrait.png', kind: 'Doodad'}
+ {slug: 'stalactite-3', name: 'Stalactite 3', original: '55760f6f1e82182d9e68891a', portraitURL: '/file/db/thang.type/55760f6f1e82182d9e68891a/portrait.png', kind: 'Doodad'}
+ {slug: 'stalagmite-1', name: 'Stalagmite 1', original: '55760e6f1e82182d9e688906', portraitURL: '/file/db/thang.type/55760e6f1e82182d9e688906/portrait.png', kind: 'Doodad'}
+ {slug: 'stalagmite-2', name: 'Stalagmite 2', original: '55760eb61e82182d9e68890a', portraitURL: '/file/db/thang.type/55760eb61e82182d9e68890a/portrait.png', kind: 'Doodad'}
+ {slug: 'stalagmite-3', name: 'Stalagmite 3', original: '55760ee51e82182d9e68890e', portraitURL: '/file/db/thang.type/55760ee51e82182d9e68890e/portrait.png', kind: 'Doodad'}
+ {slug: 'statue-stone-hooded', name: 'Statue Stone Hooded', original: '546e23469df4a17d0d449ba9', portraitURL: '/file/db/thang.type/546e23469df4a17d0d449ba9/portrait.png', kind: 'Doodad'}
+ {slug: 'steel-breastplate', name: 'Steel Breastplate', original: '546ab0a83777d61863292862', portraitURL: '/file/db/thang.type/546ab0a83777d61863292862/portrait.png', kind: 'Item'}
+ {slug: 'steel-helmet', name: 'Steel Helmet', original: '5441c2ed4e9aeb727cc9710b', portraitURL: '/file/db/thang.type/5441c2ed4e9aeb727cc9710b/portrait.png', kind: 'Item'}
+ {slug: 'steel-ring', name: 'Steel Ring', original: '54692dbca2b1f53ce794439b', portraitURL: '/file/db/thang.type/54692dbca2b1f53ce794439b/portrait.png', kind: 'Item'}
+ {slug: 'steel-shield', name: 'Steel Shield', original: '544d7bec8494308424f56497', portraitURL: '/file/db/thang.type/544d7bec8494308424f56497/portrait.png', kind: 'Item'}
+ {slug: 'steel-striker', name: 'Steel Striker', original: '544d7c948494308424f5649f', portraitURL: '/file/db/thang.type/544d7c948494308424f5649f/portrait.png', kind: 'Item'}
+ {slug: 'steel-wand', name: 'Steel Wand', original: '544d88e48494308424f56511', portraitURL: '/file/db/thang.type/544d88e48494308424f56511/portrait.png', kind: 'Item'}
+ {slug: 'stiff-lambswool-hat', name: 'Stiff Lambswool Hat', original: '546d4b379df4a17d0d449aa7', portraitURL: '/file/db/thang.type/546d4b379df4a17d0d449aa7/portrait.png', kind: 'Item'}
+ {slug: 'stone-builders-hammer', name: 'Stone Builder\'s Hammer', original: '54694bcca2b1f53ce7944451', portraitURL: '/file/db/thang.type/54694bcca2b1f53ce7944451/portrait.png', kind: 'Item'}
+ {slug: 'stone-fall-1', name: 'Stone Fall 1', original: '53e2e5046f406a3505b3ead6', portraitURL: '/file/db/thang.type/53e2e5046f406a3505b3ead6/portrait.png', kind: 'Doodad'}
+ {slug: 'stone-fall-2', name: 'Stone Fall 2', original: '53e2e66d6c59f534050410d0', portraitURL: '/file/db/thang.type/53e2e66d6c59f534050410d0/portrait.png', kind: 'Doodad'}
+ {slug: 'stone-fall-3', name: 'Stone Fall 3', original: '53e2e728ae44ec37059f2438', portraitURL: '/file/db/thang.type/53e2e728ae44ec37059f2438/portrait.png', kind: 'Doodad'}
+ {slug: 'stone-pillars', name: 'stone pillars', original: '572d5958f5da8e29013e4e8d', portraitURL: '/file/db/thang.type/572d5958f5da8e29013e4e8d/portrait.png', kind: 'Doodad'}
+ {slug: 'stone-statue', name: 'Stone Statue', original: '546e25479df4a17d0d449bd5', portraitURL: '/file/db/thang.type/546e25479df4a17d0d449bd5/portrait.png', kind: 'Doodad'}
+ {slug: 'stormbringer', name: 'Stormbringer', original: '54ea87342b7506e891ca7175', portraitURL: '/file/db/thang.type/54ea87342b7506e891ca7175/portrait.png', kind: 'Item'}
+ {slug: 'stretched-hide', name: 'Stretched Hide', original: '557608901e82182d9e6888ce', portraitURL: '/file/db/thang.type/557608901e82182d9e6888ce/portrait.png', kind: 'Doodad'}
+ {slug: 'student-a', name: 'Student A', original: '56d0edd0441ddd2f002ba5aa', portraitURL: '/file/db/thang.type/56d0edd0441ddd2f002ba5aa/portrait.png', kind: 'Unit'}
+ {slug: 'student-b', name: 'Student B', original: '56d0efc14292981f009f51de', portraitURL: '/file/db/thang.type/56d0efc14292981f009f51de/portrait.png', kind: 'Unit'}
+ {slug: 'stump-1', name: 'Stump 1', original: '54e955f6f54ef5794f354f09', portraitURL: '/file/db/thang.type/54e955f6f54ef5794f354f09/portrait.png', kind: 'Doodad'}
+ {slug: 'stump-2', name: 'Stump 2', original: '54e95634f54ef5794f354f0d', portraitURL: '/file/db/thang.type/54e95634f54ef5794f354f0d/portrait.png', kind: 'Doodad'}
+ {slug: 'stump-3', name: 'Stump 3', original: '557f91f9b43ce0b15a91b1cd', portraitURL: '/file/db/thang.type/557f91f9b43ce0b15a91b1cd/portrait.png', kind: 'Doodad'}
+ {slug: 'stump-4', name: 'Stump 4', original: '557f923eb43ce0b15a91b1d1', portraitURL: '/file/db/thang.type/557f923eb43ce0b15a91b1d1/portrait.png', kind: 'Doodad'}
+ {slug: 'stump-5', name: 'Stump 5', original: '557f925ab43ce0b15a91b1d5', portraitURL: '/file/db/thang.type/557f925ab43ce0b15a91b1d5/portrait.png', kind: 'Doodad'}
+ {slug: 'sturdy-bronze-shield', name: 'Sturdy Bronze Shield', original: '544d7b028494308424f5648b', portraitURL: '/file/db/thang.type/544d7b028494308424f5648b/portrait.png', kind: 'Item'}
+ {slug: 'sulphur-staff', name: 'Sulphur Staff', original: '54eab7132b7506e891ca71fa', portraitURL: '/file/db/thang.type/54eab7132b7506e891ca71fa/portrait.png', kind: 'Item'}
+ {slug: 'sundial-wristwatch', name: 'Sundial Wristwatch', original: '53e2396a53457600003e3f0f', portraitURL: '/file/db/thang.type/53e2396a53457600003e3f0f/portrait.png', kind: 'Item'}
+ {slug: 'sword', name: 'Sword', original: '52bcda141f766a891c00000a', portraitURL: '/file/db/thang.type/52bcda141f766a891c00000a/portrait.png', kind: 'Misc'}
+ {slug: 'sword-belt', name: 'Sword Belt', original: '5441beb74e9aeb727cc970d3', portraitURL: '/file/db/thang.type/5441beb74e9aeb727cc970d3/portrait.png', kind: 'Item'}
+ {slug: 'sword-fall-1', name: 'Sword Fall 1', original: '53e2e8a7d12e873205b6c0f1', portraitURL: '/file/db/thang.type/53e2e8a7d12e873205b6c0f1/portrait.png', kind: 'Doodad'}
+ {slug: 'sword-fall-2', name: 'Sword Fall 2', original: '53e2e9a9ae44ec37059f2571', portraitURL: '/file/db/thang.type/53e2e9a9ae44ec37059f2571/portrait.png', kind: 'Doodad'}
+ {slug: 'sword-of-the-forgotten', name: 'Sword of the Forgotten', original: '54eaaa522b7506e891ca71b9', portraitURL: '/file/db/thang.type/54eaaa522b7506e891ca71b9/portrait.png', kind: 'Item'}
+ {slug: 'sword-of-the-temple-guard', name: 'Sword of the Temple Guard', original: '54eaab372b7506e891ca71c1', portraitURL: '/file/db/thang.type/54eaab372b7506e891ca71c1/portrait.png', kind: 'Item'}
+ {slug: 'table', name: 'Table', original: '52e9987a427172ae56001ffd', portraitURL: '/file/db/thang.type/52e9987a427172ae56001ffd/portrait.png', kind: 'Doodad'}
+ {slug: 'tailored-linen-robe', name: 'Tailored Linen Robe', original: '546d49759df4a17d0d449a87', portraitURL: '/file/db/thang.type/546d49759df4a17d0d449a87/portrait.png', kind: 'Item'}
+ {slug: 'talus-1', name: 'Talus 1', original: '54e944a3f54ef5794f354ea9', portraitURL: '/file/db/thang.type/54e944a3f54ef5794f354ea9/portrait.png', kind: 'Floor'}
+ {slug: 'talus-2', name: 'Talus 2', original: '54e94880f54ef5794f354ead', portraitURL: '/file/db/thang.type/54e94880f54ef5794f354ead/portrait.png', kind: 'Floor'}
+ {slug: 'talus-3', name: 'Talus 3', original: '54e948daf54ef5794f354eb1', portraitURL: '/file/db/thang.type/54e948daf54ef5794f354eb1/portrait.png', kind: 'Floor'}
+ {slug: 'talus-4', name: 'Talus 4', original: '54e94908f54ef5794f354eb5', portraitURL: '/file/db/thang.type/54e94908f54ef5794f354eb5/portrait.png', kind: 'Floor'}
+ {slug: 'talus-5', name: 'Talus 5', original: '54e9493cf54ef5794f354eb9', portraitURL: '/file/db/thang.type/54e9493cf54ef5794f354eb9/portrait.png', kind: 'Floor'}
+ {slug: 'talus-6', name: 'Talus 6', original: '54e94965f54ef5794f354ebd', portraitURL: '/file/db/thang.type/54e94965f54ef5794f354ebd/portrait.png', kind: 'Floor'}
+ {slug: 'tarnished-bronze-breastplate', name: 'Tarnished Bronze Breastplate', original: '53e22eac53457600003e3efc', portraitURL: '/file/db/thang.type/53e22eac53457600003e3efc/portrait.png', kind: 'Item'}
+ {slug: 'tarnished-bronze-helmet', name: 'Tarnished Bronze Helmet', original: '546d38269df4a17d0d4499ff', portraitURL: '/file/db/thang.type/546d38269df4a17d0d4499ff/portrait.png', kind: 'Item'}
+ {slug: 'tarnished-copper-band', name: 'Tarnished Copper Band', original: '54692a75a2b1f53ce7944387', portraitURL: '/file/db/thang.type/54692a75a2b1f53ce7944387/portrait.png', kind: 'Item'}
+ {slug: 'tauran-helm', name: 'Tauran Helm', original: '54ea49982b7506e891ca7165', portraitURL: '/file/db/thang.type/54ea49982b7506e891ca7165/portrait.png', kind: 'Item'}
+ {slug: 'tauran-plate', name: 'Tauran Plate', original: '54ea4b302b7506e891ca716d', portraitURL: '/file/db/thang.type/54ea4b302b7506e891ca716d/portrait.png', kind: 'Item'}
+ {slug: 'teacher-b', name: 'Teacher B', original: '56de0554d048927700b4f741', portraitURL: '/file/db/thang.type/56de0554d048927700b4f741/portrait.png', kind: 'Doodad'}
+ {slug: 'tent-1', name: 'Tent 1', original: '548cf2280f559d0000be7e37', portraitURL: '/file/db/thang.type/548cf2280f559d0000be7e37/portrait.png', kind: 'Doodad'}
+ {slug: 'tent-2', name: 'Tent 2', original: '548cf2b10f559d0000be7e3b', portraitURL: '/file/db/thang.type/548cf2b10f559d0000be7e3b/portrait.png', kind: 'Doodad'}
+ {slug: 'tent-3', name: 'Tent 3', original: '548cf30b0f559d0000be7e3f', portraitURL: '/file/db/thang.type/548cf30b0f559d0000be7e3f/portrait.png', kind: 'Doodad'}
+ {slug: 'the-final-kithmaze-background', name: 'the final kithmaze background', original: '577ecc2b67053f25007eb916', portraitURL: '/file/db/thang.type/577ecc2b67053f25007eb916/portrait.png', kind: 'Floor'}
+ {slug: 'the-gauntlet-background', name: 'The Gauntlet Background', original: '572d631812f2abce00164c15', portraitURL: '/file/db/thang.type/572d631812f2abce00164c15/portrait.png', kind: 'Floor'}
+ {slug: 'the-monolith', name: 'The Monolith', original: '54eabcb72b7506e891ca7226', portraitURL: '/file/db/thang.type/54eabcb72b7506e891ca7226/portrait.png', kind: 'Item'}
+ {slug: 'the-precious', name: 'The Precious', original: '54eb56ae49fa2d5c905ddf2a', portraitURL: '/file/db/thang.type/54eb56ae49fa2d5c905ddf2a/portrait.png', kind: 'Item'}
+ {slug: 'thick-burlap-robe', name: 'Thick Burlap Robe', original: '546d48989df4a17d0d449a77', portraitURL: '/file/db/thang.type/546d48989df4a17d0d449a77/portrait.png', kind: 'Item'}
+ {slug: 'thin-burlap-robe', name: 'Thin Burlap Robe', original: '546d485b9df4a17d0d449a73', portraitURL: '/file/db/thang.type/546d485b9df4a17d0d449a73/portrait.png', kind: 'Item'}
+ {slug: 'thoktars-discarded-hammer', name: 'Thoktar\'s Discarded Hammer', original: '54694cd6a2b1f53ce7944466', portraitURL: '/file/db/thang.type/54694cd6a2b1f53ce7944466/portrait.png', kind: 'Item'}
+ {slug: 'thornprick', name: 'Thornprick', original: '54692e75a2b1f53ce79443a7', portraitURL: '/file/db/thang.type/54692e75a2b1f53ce79443a7/portrait.png', kind: 'Item'}
+ {slug: 'threadbare-burlap-wizards-hat', name: 'Threadbare Burlap Wizards Hat', original: '546d4a909df4a17d0d449a9b', portraitURL: '/file/db/thang.type/546d4a909df4a17d0d449a9b/portrait.png', kind: 'Item'}
+ {slug: 'throne', name: 'Throne', original: '54efa174933e1e7b05846fe6', portraitURL: '/file/db/thang.type/54efa174933e1e7b05846fe6/portrait.png', kind: 'Doodad'}
+ {slug: 'tomb-ring', name: 'Tomb Ring', original: '54eb55d849fa2d5c905ddf26', portraitURL: '/file/db/thang.type/54eb55d849fa2d5c905ddf26/portrait.png', kind: 'Item'}
+ {slug: 'tool-belt', name: 'Tool Belt', original: '5441beff4e9aeb727cc970d9', portraitURL: '/file/db/thang.type/5441beff4e9aeb727cc970d9/portrait.png', kind: 'Item'}
+ {slug: 'torch', name: 'Torch', original: '52aa608b20fccb0000000005', portraitURL: '/file/db/thang.type/52aa608b20fccb0000000005/portrait.png', kind: 'Doodad'}
+ {slug: 'torn-silk-cloak', name: 'Torn Silk Cloak', original: '546d49a79df4a17d0d449a8b', portraitURL: '/file/db/thang.type/546d49a79df4a17d0d449a8b/portrait.png', kind: 'Item'}
+ {slug: 'torture-table', name: 'Torture Table', original: '54ef8fd4b474077905843564', portraitURL: '/file/db/thang.type/54ef8fd4b474077905843564/portrait.png', kind: 'Doodad'}
+ {slug: 'tower-ruined', name: 'Tower Ruined', original: '54f117c548724e7d052b540b', portraitURL: '/file/db/thang.type/54f117c548724e7d052b540b/portrait.png', kind: 'Doodad'}
+ {slug: 'training-dummy', name: 'Training Dummy', original: '53e65923bc5cc012113e07b1', portraitURL: '/file/db/thang.type/53e65923bc5cc012113e07b1/portrait.png', kind: 'Doodad'}
+ {slug: 'trap-belt', name: 'Trap Belt', original: '54694a8fa2b1f53ce7944439', portraitURL: '/file/db/thang.type/54694a8fa2b1f53ce7944439/portrait.png', kind: 'Item'}
+ {slug: 'treasure-chest', name: 'Treasure Chest', original: '52aa3be0ccbd588d4d000005', portraitURL: '/file/db/thang.type/52aa3be0ccbd588d4d000005/portrait.png', kind: 'Doodad'}
+ {slug: 'tree-1', name: 'Tree 1', original: '52b09ef7ccbc67137200000f', portraitURL: '/file/db/thang.type/52b09ef7ccbc67137200000f/portrait.png', kind: 'Doodad'}
+ {slug: 'tree-2', name: 'Tree 2', original: '52b09fdeccbc671372000011', portraitURL: '/file/db/thang.type/52b09fdeccbc671372000011/portrait.png', kind: 'Doodad'}
+ {slug: 'tree-3', name: 'Tree 3', original: '52b0a04fccbc671372000013', portraitURL: '/file/db/thang.type/52b0a04fccbc671372000013/portrait.png', kind: 'Doodad'}
+ {slug: 'tree-4', name: 'Tree 4', original: '52b0a0a5ccbc671372000015', portraitURL: '/file/db/thang.type/52b0a0a5ccbc671372000015/portrait.png', kind: 'Doodad'}
+ {slug: 'tree-stand-1', name: 'Tree Stand 1', original: '541cc7c48e78524aad94de7d', portraitURL: '/file/db/thang.type/541cc7c48e78524aad94de7d/portrait.png', kind: 'Doodad'}
+ {slug: 'tree-stand-2', name: 'Tree Stand 2', original: '542068f38e78524aad94de83', portraitURL: '/file/db/thang.type/542068f38e78524aad94de83/portrait.png', kind: 'Doodad'}
+ {slug: 'tree-stand-3', name: 'Tree Stand 3', original: '5420693d8e78524aad94de89', portraitURL: '/file/db/thang.type/5420693d8e78524aad94de89/portrait.png', kind: 'Doodad'}
+ {slug: 'tree-stand-4', name: 'Tree Stand 4', original: '542069888e78524aad94de8f', portraitURL: '/file/db/thang.type/542069888e78524aad94de8f/portrait.png', kind: 'Doodad'}
+ {slug: 'tree-stand-5', name: 'Tree Stand 5', original: '542092628e78524aad94deca', portraitURL: '/file/db/thang.type/542092628e78524aad94deca/portrait.png', kind: 'Doodad'}
+ {slug: 'tree-stand-6', name: 'Tree Stand 6', original: '542092c38e78524aad94ded0', portraitURL: '/file/db/thang.type/542092c38e78524aad94ded0/portrait.png', kind: 'Doodad'}
+ {slug: 'true-names-background', name: 'True Names Background', original: '55e451bd206f7df7df6ba966', portraitURL: '/file/db/thang.type/55e451bd206f7df7df6ba966/portrait.png', kind: 'Floor'}
+ {slug: 'twilight-glasses', name: 'Twilight Glasses', original: '546941fda2b1f53ce794441d', portraitURL: '/file/db/thang.type/546941fda2b1f53ce794441d/portrait.png', kind: 'Item'}
+ {slug: 'twisted-pine-wand', name: 'Twisted Pine Wand', original: '544d877d8494308424f564f9', portraitURL: '/file/db/thang.type/544d877d8494308424f564f9/portrait.png', kind: 'Item'}
+ {slug: 'undead', name: 'Undead', original: '55c284933767fd3435eb4471', portraitURL: '/file/db/thang.type/55c284933767fd3435eb4471/portrait.png', kind: 'Mark'}
+ {slug: 'undergrowth-dagger', name: 'Undergrowth Dagger', original: '544d95e68494308424f5652b', portraitURL: '/file/db/thang.type/544d95e68494308424f5652b/portrait.png', kind: 'Item'}
+ {slug: 'undergrowth-dagger-missile', name: 'Undergrowth Dagger Missile', original: '544d99618494308424f56541', portraitURL: '/file/db/thang.type/544d99618494308424f56541/portrait.png', kind: 'Missile'}
+ {slug: 'undying-ring', name: 'Undying Ring', original: '54eb54d349fa2d5c905ddf1e', portraitURL: '/file/db/thang.type/54eb54d349fa2d5c905ddf1e/portrait.png', kind: 'Item'}
+ {slug: 'unholy-tome-i', name: 'Unholy Tome I', original: '546374bc3839c6e02811d308', portraitURL: '/file/db/thang.type/546374bc3839c6e02811d308/portrait.png', kind: 'Item'}
+ {slug: 'unholy-tome-ii', name: 'Unholy Tome II', original: '5463756f3839c6e02811d30c', portraitURL: '/file/db/thang.type/5463756f3839c6e02811d30c/portrait.png', kind: 'Item'}
+ {slug: 'unholy-tome-iii', name: 'Unholy Tome III', original: '5463758f3839c6e02811d30f', portraitURL: '/file/db/thang.type/5463758f3839c6e02811d30f/portrait.png', kind: 'Item'}
+ {slug: 'unholy-tome-iv', name: 'Unholy Tome IV', original: '546376b63839c6e02811d31b', portraitURL: '/file/db/thang.type/546376b63839c6e02811d31b/portrait.png', kind: 'Item'}
+ {slug: 'unholy-tome-v', name: 'Unholy Tome V', original: '546376da3839c6e02811d31e', portraitURL: '/file/db/thang.type/546376da3839c6e02811d31e/portrait.png', kind: 'Item'}
+ {slug: 'viking-helmet', name: 'Viking Helmet', original: '5441c3144e9aeb727cc97111', portraitURL: '/file/db/thang.type/5441c3144e9aeb727cc97111/portrait.png', kind: 'Item'}
+ {slug: 'viking-helmet-doodad', name: 'Viking Helmet Doodad', original: '5518239d1f12482609b44f76', portraitURL: '/file/db/thang.type/5518239d1f12482609b44f76/portrait.png', kind: 'Doodad'}
+ {slug: 'vine-staff', name: 'Vine Staff', original: '54eab92b2b7506e891ca720a', portraitURL: '/file/db/thang.type/54eab92b2b7506e891ca720a/portrait.png', kind: 'Item'}
+ {slug: 'volcano', name: 'Volcano', original: '55c64512ef141c65665beb7e', portraitURL: '/file/db/thang.type/55c64512ef141c65665beb7e/portrait.png', kind: 'Doodad'}
+ {slug: 'vr-artist', name: 'VR Artist', original: '56d0c6bf087ee32400763d49', portraitURL: '/file/db/thang.type/56d0c6bf087ee32400763d49/portrait.png', kind: 'Unit'}
+ {slug: 'vr-breaker', name: 'VR Breaker', original: '56d0e6e563103d2a00af5795', portraitURL: '/file/db/thang.type/56d0e6e563103d2a00af5795/portrait.png', kind: 'Unit'}
+ {slug: 'vr-door', name: 'VR Door', original: '56aa6bf503ec4e2000878867', portraitURL: '/file/db/thang.type/56aa6bf503ec4e2000878867/portrait.png', kind: 'Doodad'}
+ {slug: 'vr-floor', name: 'VR Floor', original: '56a2e305b0b7242000e9986e', portraitURL: '/file/db/thang.type/56a2e305b0b7242000e9986e/portrait.png', kind: 'Floor'}
+ {slug: 'vr-oracle', name: 'VR Oracle', original: '56d0d144a7daf22000023a13', portraitURL: '/file/db/thang.type/56d0d144a7daf22000023a13/portrait.png', kind: 'Unit'}
+ {slug: 'vr-security', name: 'VR Security', original: '56d758b787781b1f00cf4b20', portraitURL: '/file/db/thang.type/56d758b787781b1f00cf4b20/portrait.png', kind: 'Unit'}
+ {slug: 'vr-tinker', name: 'VR Tinker', original: '56d07d682a1e1736005b1b37', portraitURL: '/file/db/thang.type/56d07d682a1e1736005b1b37/portrait.png', kind: 'Unit'}
+ {slug: 'vr-wall', name: 'vr-wall', original: '56b0c75302b7db290079b542', portraitURL: '/file/db/thang.type/56b0c75302b7db290079b542/portrait.png', kind: 'Wall'}
+ {slug: 'vr-wyrm', name: 'VR Wyrm', original: '56bb944d203af82000b2a406', portraitURL: '/file/db/thang.type/56bb944d203af82000b2a406/portrait.png', kind: 'Unit'}
+ {slug: 'wagon-broken', name: 'Wagon Broken', original: '548cf1cd0f559d0000be7e33', portraitURL: '/file/db/thang.type/548cf1cd0f559d0000be7e33/portrait.png', kind: 'Doodad'}
+ {slug: 'wakka-maul-background', name: 'Wakka Maul Background', original: '5654eae2f9285e86053f7504', portraitURL: '/file/db/thang.type/5654eae2f9285e86053f7504/portrait.png', kind: 'Floor'}
+ {slug: 'warcry', name: 'Warcry', original: '53024777222f73867774d6cd', portraitURL: '/file/db/thang.type/53024777222f73867774d6cd/portrait.png', kind: 'Mark'}
+ {slug: 'waterfall', name: 'Waterfall', original: '53e2eaffae44ec37059f262a', portraitURL: '/file/db/thang.type/53e2eaffae44ec37059f262a/portrait.png', kind: 'Doodad'}
+ {slug: 'weak-charge', name: 'Weak Charge', original: '544d957d8494308424f5651f', portraitURL: '/file/db/thang.type/544d957d8494308424f5651f/portrait.png', kind: 'Item'}
+ {slug: 'weak-charge-missile', name: 'Weak Charge Missile', original: '544d97798494308424f5653b', portraitURL: '/file/db/thang.type/544d97798494308424f5653b/portrait.png', kind: 'Missile'}
+ {slug: 'weighted-throwing-knives', name: 'Weighted Throwing Knives', original: '544d96108494308424f5652f', portraitURL: '/file/db/thang.type/544d96108494308424f5652f/portrait.png', kind: 'Item'}
+ {slug: 'weighted-throwing-knives-missile', name: 'Weighted Throwing Knives Missile', original: '544d99b98494308424f56545', portraitURL: '/file/db/thang.type/544d99b98494308424f56545/portrait.png', kind: 'Missile'}
+ {slug: 'well', name: 'Well', original: '52b094cbccbc671372000004', portraitURL: '/file/db/thang.type/52b094cbccbc671372000004/portrait.png', kind: 'Doodad'}
+ {slug: 'white-deerhide-gloves', name: 'White Deerhide Gloves', original: '54694936a2b1f53ce7944429', portraitURL: '/file/db/thang.type/54694936a2b1f53ce7944429/portrait.png', kind: 'Item'}
+ {slug: 'windwalker-coif', name: 'Windwalker Coif', original: '54ea48512b7506e891ca7157', portraitURL: '/file/db/thang.type/54ea48512b7506e891ca7157/portrait.png', kind: 'Item'}
+ {slug: 'windwalker-mail', name: 'Windwalker Mail', original: '54ea46092b7506e891ca7143', portraitURL: '/file/db/thang.type/54ea46092b7506e891ca7143/portrait.png', kind: 'Item'}
+ {slug: 'winged-boots', name: 'Winged Boots', original: '546d4e5c9df4a17d0d449ad9', portraitURL: '/file/db/thang.type/546d4e5c9df4a17d0d449ad9/portrait.png', kind: 'Item'}
+ {slug: 'wizard-bird-f', name: 'Wizard Bird F', original: '52fc0c9e7e01835453bd8ef8', portraitURL: '/file/db/thang.type/52fc0c9e7e01835453bd8ef8/portrait.png', kind: 'Unit'}
+ {slug: 'wizard-bird-m', name: 'Wizard Bird M', original: '52fd015f3a58c6c50fcf4782', portraitURL: '/file/db/thang.type/52fd015f3a58c6c50fcf4782/portrait.png', kind: 'Unit'}
+ {slug: 'wizard-doctor', name: 'Wizard Doctor', original: '52fc04fbab6e45c813bc7ced', portraitURL: '/file/db/thang.type/52fc04fbab6e45c813bc7ced/portrait.png', kind: 'Unit'}
+ {slug: 'wizard-dude', name: 'Wizard Dude', original: '53e126a4e06b897606d38bef', portraitURL: '/file/db/thang.type/53e126a4e06b897606d38bef/portrait.png', kind: 'Unit'}
+ {slug: 'wizard-hermes', name: 'Wizard Hermes', original: '52fc09daab6e45c813bc7d52', portraitURL: '/file/db/thang.type/52fc09daab6e45c813bc7d52/portrait.png', kind: 'Unit'}
+ {slug: 'wizard-knight', name: 'Wizard Knight', original: '52fc00ffab6e45c813bc7cb2', portraitURL: '/file/db/thang.type/52fc00ffab6e45c813bc7cb2/portrait.png', kind: 'Unit'}
+ {slug: 'wizard-ninja-m', name: 'Wizard Ninja M', original: '52fd04aff0cd954d619a9a4c', portraitURL: '/file/db/thang.type/52fd04aff0cd954d619a9a4c/portrait.png', kind: 'Unit'}
+ {slug: 'wizard-overseer-f', name: 'Wizard Overseer F', original: '52fc11fbb2b91c0d5a7b6a14', portraitURL: '/file/db/thang.type/52fc11fbb2b91c0d5a7b6a14/portrait.png', kind: 'Unit'}
+ {slug: 'wizard-overseer-m', name: 'Wizard Overseer M', original: '52fd0728ccb2653821eaf8b0', portraitURL: '/file/db/thang.type/52fd0728ccb2653821eaf8b0/portrait.png', kind: 'Unit'}
+ {slug: 'wizard-purple', name: 'Wizard Purple', original: '52fd0e16c7e6cf99160e7b6a', portraitURL: '/file/db/thang.type/52fd0e16c7e6cf99160e7b6a/portrait.png', kind: 'Unit'}
+ {slug: 'wizard-spine', name: 'Wizard Spine', original: '52fcfed63a58c6c50fcf4732', portraitURL: '/file/db/thang.type/52fcfed63a58c6c50fcf4732/portrait.png', kind: 'Unit'}
+ {slug: 'wizard-spine-m', name: 'Wizard Spine M', original: '52fd0c70f0cd954d619a9b10', portraitURL: '/file/db/thang.type/52fd0c70f0cd954d619a9b10/portrait.png', kind: 'Unit'}
+ {slug: 'wizard-thorn-f', name: 'Wizard Thorn F', original: '52fc1460b2b91c0d5a7b6af3', portraitURL: '/file/db/thang.type/52fc1460b2b91c0d5a7b6af3/portrait.png', kind: 'Unit'}
+ {slug: 'wizard-thorn-m', name: 'Wizard Thorn M', original: '52fd0a40f0cd954d619a9ad7', portraitURL: '/file/db/thang.type/52fd0a40f0cd954d619a9ad7/portrait.png', kind: 'Unit'}
+ {slug: 'wizard-top-hat', name: 'Wizard Top Hat', original: '52fd124accb2653821eaf991', portraitURL: '/file/db/thang.type/52fd124accb2653821eaf991/portrait.png', kind: 'Unit'}
+ {slug: 'wooden-builders-hammer', name: 'Wooden Builder\'s Hammer', original: '54694ba3a2b1f53ce794444d', portraitURL: '/file/db/thang.type/54694ba3a2b1f53ce794444d/portrait.png', kind: 'Item'}
+ {slug: 'wooden-glasses', name: 'Wooden Glasses', original: '53e2167653457600003e3eb3', portraitURL: '/file/db/thang.type/53e2167653457600003e3eb3/portrait.png', kind: 'Item'}
+ {slug: 'wooden-shield', name: 'Wooden Shield', original: '53e22aa153457600003e3ef5', portraitURL: '/file/db/thang.type/53e22aa153457600003e3ef5/portrait.png', kind: 'Item'}
+ {slug: 'wooden-strand', name: 'Wooden Strand', original: '54692e3ea2b1f53ce79443a3', portraitURL: '/file/db/thang.type/54692e3ea2b1f53ce79443a3/portrait.png', kind: 'Item'}
+ {slug: 'workers-gloves', name: 'Worker\'s Gloves', original: '5469425ca2b1f53ce7944421', portraitURL: '/file/db/thang.type/5469425ca2b1f53ce7944421/portrait.png', kind: 'Item'}
+ {slug: 'worn-dragonplate', name: 'Worn Dragonplate', original: '546ab1a13777d61863292872', portraitURL: '/file/db/thang.type/546ab1a13777d61863292872/portrait.png', kind: 'Item'}
+ {slug: 'worn-dragonplate-helmet', name: 'Worn Dragonplate Helmet', original: '546d3a199df4a17d0d449a1b', portraitURL: '/file/db/thang.type/546d3a199df4a17d0d449a1b/portrait.png', kind: 'Item'}
+ {slug: 'worn-dragonshield', name: 'Worn Dragonshield', original: '54eabd662b7506e891ca722e', portraitURL: '/file/db/thang.type/54eabd662b7506e891ca722e/portrait.png', kind: 'Item'}
+ {slug: 'wyrm2', name: 'wyrm2', original: '56c32fd1807b9f36005e5fd0', portraitURL: '/file/db/thang.type/56c32fd1807b9f36005e5fd0/portrait.png', kind: 'Unit'}
+ {slug: 'wyvernclaw', name: 'Wyvernclaw', original: '54ea35fd2b7506e891ca70d5', portraitURL: '/file/db/thang.type/54ea35fd2b7506e891ca70d5/portrait.png', kind: 'Item'}
+ {slug: 'x-mark-bones', name: 'X Mark Bones', original: '54938352e9850ae3e8fbdd64', portraitURL: '/file/db/thang.type/54938352e9850ae3e8fbdd64/portrait.png', kind: 'Doodad'}
+ {slug: 'x-mark-forest', name: 'X Mark Forest', original: '549381a7e9850ae3e8fbdd60', portraitURL: '/file/db/thang.type/549381a7e9850ae3e8fbdd60/portrait.png', kind: 'Doodad'}
+ {slug: 'x-mark-red', name: 'X Mark Red', original: '5493844be9850ae3e8fbdd70', portraitURL: '/file/db/thang.type/5493844be9850ae3e8fbdd70/portrait.png', kind: 'Doodad'}
+ {slug: 'x-mark-stone', name: 'X Mark Stone', original: '549383aae9850ae3e8fbdd68', portraitURL: '/file/db/thang.type/549383aae9850ae3e8fbdd68/portrait.png', kind: 'Doodad'}
+ {slug: 'x-mark-wood', name: 'X Mark Wood', original: '54938408e9850ae3e8fbdd6c', portraitURL: '/file/db/thang.type/54938408e9850ae3e8fbdd6c/portrait.png', kind: 'Doodad'}
+ {slug: 'x-marker', name: 'X Marker', original: '5452ec9f06a59e000067e518', portraitURL: '/file/db/thang.type/5452ec9f06a59e000067e518/portrait.png', kind: 'Doodad'}
+ {slug: 'x-ray-goggles', name: 'X-Ray Goggles', original: '53e2392453457600003e3f0d', portraitURL: '/file/db/thang.type/53e2392453457600003e3f0d/portrait.png', kind: 'Item'}
+ {slug: 'yeti', name: 'Yeti', original: '54e91dc5970f0b0a263c03de', portraitURL: '/file/db/thang.type/54e91dc5970f0b0a263c03de/portrait.png', kind: 'Unit'}
+ {slug: 'yeti-cave', name: 'Yeti Cave', original: '557f8f84b43ce0b15a91b1c7', portraitURL: '/file/db/thang.type/557f8f84b43ce0b15a91b1c7/portrait.png', kind: 'Doodad'}
+ {slug: 'yeti-skin', name: 'Yeti Skin', original: '557f370ab43ce0b15a91b171', portraitURL: '/file/db/thang.type/557f370ab43ce0b15a91b171/portrait.png', kind: 'Doodad'}
+ ]
diff --git a/app/views/play/level/tome/SpellTopBarView.coffee b/app/views/play/level/tome/SpellTopBarView.coffee
index fe1814df2..7c8051f86 100644
--- a/app/views/play/level/tome/SpellTopBarView.coffee
+++ b/app/views/play/level/tome/SpellTopBarView.coffee
@@ -1,6 +1,7 @@
template = require 'templates/play/level/tome/spell-top-bar-view'
ReloadLevelModal = require 'views/play/level/modal/ReloadLevelModal'
CocoView = require 'views/core/CocoView'
+ImageGalleryModal = require 'views/play/level/modal/ImageGalleryModal'
module.exports = class SpellTopBarView extends CocoView
template: template
@@ -20,6 +21,7 @@ module.exports = class SpellTopBarView extends CocoView
'click .beautify-code': 'onBeautifyClick'
'click .fullscreen-code': 'onToggleMaximize'
'click .hints-button': 'onClickHintsButton'
+ 'click .image-gallery-button': 'onClickImageGalleryButton'
constructor: (options) ->
@hintsState = options.hintsState
@@ -43,6 +45,9 @@ module.exports = class SpellTopBarView extends CocoView
onDisableControls: (e) -> @toggleControls e, false
onEnableControls: (e) -> @toggleControls e, true
+ onClickImageGalleryButton: (e) ->
+ @openModalView new ImageGalleryModal()
+
onClickHintsButton: ->
return unless @hintsState?
@hintsState.set('hidden', not @hintsState.get('hidden'))
From 5d0b9c875acc1aa20ae8424ef7572081828e72ca Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Sat, 16 Jul 2016 13:32:54 -0700
Subject: [PATCH 47/58] Fix some typos that made it not work in Firefox (not
sure how it worked in Chrome)
---
app/assets/javascripts/web-dev-listener.js | 8 ++++----
app/views/play/level/WebSurfaceView.coffee | 4 ++--
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/app/assets/javascripts/web-dev-listener.js b/app/assets/javascripts/web-dev-listener.js
index b6fea6acd..96f0e19ae 100644
--- a/app/assets/javascripts/web-dev-listener.js
+++ b/app/assets/javascripts/web-dev-listener.js
@@ -48,8 +48,8 @@ function receiveMessage(event) {
}
function create(dom) {
- concreteDOM = deku.dom.create(event.data.dom);
- virtualDOM = event.data.dom;
+ virtualDOM = dom;
+ concreteDOM = deku.dom.create(dom);
// TODO: target the actual HTML tag and combine our initial structure for styles/scripts/tags with theirs
$('body').empty().append(concreteDOM);
}
@@ -57,9 +57,9 @@ function create(dom) {
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);
+ var changes = deku.diff.diffNode(virtualDOM, dom);
changes.reduce(deku.dom.update(dispatch, context), concreteDOM); // Rerender
- virtualDOM = event.data.dom;
+ virtualDOM = dom;
}
function checkGoals(goals, source, origin) {
diff --git a/app/views/play/level/WebSurfaceView.coffee b/app/views/play/level/WebSurfaceView.coffee
index 732468af9..fa3e374be 100644
--- a/app/views/play/level/WebSurfaceView.coffee
+++ b/app/views/play/level/WebSurfaceView.coffee
@@ -48,8 +48,8 @@ module.exports = class WebSurfaceView extends CocoView
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
+ onIframeMessage: (event) =>
+ origin = event.origin or event.originalEvent.origin
unless origin is window.location.origin
return console.log 'Ignoring message from bad origin:', origin
unless event.source is @iframe.contentWindow
From b04e968da57b9b16f557248152243710c2783a3c Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Sat, 16 Jul 2016 23:17:05 -0700
Subject: [PATCH 48/58] Add support for CSS docs
---
.../play/level/tome/spell_palette_entry_popover.jade | 2 +-
app/views/play/level/tome/DocFormatter.coffee | 2 +-
app/views/play/level/tome/SpellPaletteView.coffee | 7 ++++---
3 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/app/templates/play/level/tome/spell_palette_entry_popover.jade b/app/templates/play/level/tome/spell_palette_entry_popover.jade
index 879a30225..dd5306643 100644
--- a/app/templates/play/level/tome/spell_palette_entry_popover.jade
+++ b/app/templates/play/level/tome/spell_palette_entry_popover.jade
@@ -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.owner != 'HTML') || doc.name == 'now'
+if (doc.type != 'function' && doc.type != 'snippet' && doc.owner != 'HTML' && doc.owner != 'CSS') || doc.name == 'now'
p.value
strong
span(data-i18n="skill_docs.current_value") Current Value
diff --git a/app/views/play/level/tome/DocFormatter.coffee b/app/views/play/level/tome/DocFormatter.coffee
index ef8500b77..cc890e5bf 100644
--- a/app/views/play/level/tome/DocFormatter.coffee
+++ b/app/views/play/level/tome/DocFormatter.coffee
@@ -49,7 +49,7 @@ module.exports = class DocFormatter
@doc.type = 'snippet'
@doc.owner = 'snippets'
@doc.shortName = @doc.shorterName = @doc.title = @doc.name
- else if @doc.owner is 'HTML'
+ else if @doc.owner in ['HTML', 'CSS']
@doc.shortName = @doc.shorterName = @doc.title = @doc.name
else
@doc.owner ?= 'this'
diff --git a/app/views/play/level/tome/SpellPaletteView.coffee b/app/views/play/level/tome/SpellPaletteView.coffee
index df31577d5..e8d4a6405 100644
--- a/app/views/play/level/tome/SpellPaletteView.coffee
+++ b/app/views/play/level/tome/SpellPaletteView.coffee
@@ -157,6 +157,7 @@ module.exports = class SpellPaletteView extends CocoView
LoDash: 'programmableLoDashProperties'
Vector: 'programmableVectorProperties'
HTML: 'programmableHTMLProperties'
+ CSS: 'programmableCSSProperties'
snippets: 'programmableSnippets'
else
propStorage =
@@ -196,7 +197,7 @@ 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', 'HTML', 'snippets']
+ order = ['this', 'more', 'Math', 'Vector', 'String', 'Object', 'Array', 'Function', 'HTML', 'CSS', 'snippets']
index = order.indexOf groupForEntry entry
index = String.fromCharCode if index is -1 then order.length else index
index += entry.doc.name
@@ -247,7 +248,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', 'HTML'])
+ for owner, storage of propStorage when not (owner in ['this', 'more', 'snippets', 'HTML', 'CSS'])
continue unless @thang[storage]?.length
@tabs ?= {}
@tabs[owner] = []
@@ -261,7 +262,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', 'HTML']
+ continue unless owner in ['this', 'more', 'snippets', 'HTML', 'CSS']
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
From 320aa0f3d92ef31dd674418a55c7b17a81dae59e Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Sat, 16 Jul 2016 23:30:10 -0700
Subject: [PATCH 49/58] Add first guess for other web-dev concepts
---
app/locale/en.coffee | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/app/locale/en.coffee b/app/locale/en.coffee
index c5c2138b7..63d062364 100644
--- a/app/locale/en.coffee
+++ b/app/locale/en.coffee
@@ -1880,6 +1880,14 @@
basic_html: "Basic HTML" # TODO: these web-dev concepts will change, don't need to translate
basic_css: "Basic CSS"
basic_web_scripting: "Basic Web Scripting"
+ intermediate_html: "Intermediate HTML"
+ intermediate_css: "Intermediate CSS"
+ intermediate_web_scripting: "Intermediate Web Scripting"
+ advanced_html: "Advanced HTML"
+ advanced_css: "Advanced CSS"
+ advanced_web_scripting: "Advanced Web Scripting"
+ jquery: "jQuery"
+ bootstrap: "Bootstrap"
delta:
added: "Added"
From 6e65171a836f5e04732bf323a43da935eabeb4fe Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Sun, 17 Jul 2016 00:53:17 -0700
Subject: [PATCH 50/58] i18n, comments, misc cleanup
---
app/assets/javascripts/web-dev-listener.js | 1 +
app/lib/LevelLoader.coffee | 3 +-
app/locale/en.coffee | 28 +++++++++--
app/schemas/schemas.coffee | 8 ++++
.../play/level/play-game-dev-level-view.sass | 3 ++
app/styles/play/level/web-surface-view.sass | 47 -------------------
app/templates/courses/course-details.jade | 10 ++--
app/templates/courses/teacher-class-view.jade | 17 ++++---
.../play/level/modal/hero-victory-modal.jade | 9 ++--
.../play/level/modal/image-gallery-modal.jade | 4 +-
.../play/level/modal/progress-view.jade | 14 +++---
.../play/level/play-game-dev-level-view.jade | 18 ++++---
.../play/level/play-web-dev-level-view.jade | 5 +-
.../play/level/tome/spell-top-bar-view.jade | 10 ++--
app/views/courses/CourseDetailsView.coffee | 2 +-
app/views/courses/TeacherClassView.coffee | 34 +++++++-------
.../play/level/PlayWebDevLevelView.coffee | 7 +--
app/views/play/level/WebSurfaceView.coffee | 4 --
.../play/level/modal/HeroVictoryModal.coffee | 9 ++--
app/views/play/level/tome/DocFormatter.coffee | 2 +-
app/views/play/level/tome/Spell.coffee | 3 +-
app/views/play/level/tome/TomeView.coffee | 1 -
app/views/play/menu/GuideView.coffee | 2 +-
23 files changed, 112 insertions(+), 129 deletions(-)
diff --git a/app/assets/javascripts/web-dev-listener.js b/app/assets/javascripts/web-dev-listener.js
index 96f0e19ae..3851eb316 100644
--- a/app/assets/javascripts/web-dev-listener.js
+++ b/app/assets/javascripts/web-dev-listener.js
@@ -51,6 +51,7 @@ function create(dom) {
virtualDOM = dom;
concreteDOM = deku.dom.create(dom);
// TODO: target the actual HTML tag and combine our initial structure for styles/scripts/tags with theirs
+ // TODO: :after elements don't seem to work? (:before do)
$('body').empty().append(concreteDOM);
}
diff --git a/app/lib/LevelLoader.coffee b/app/lib/LevelLoader.coffee
index 0ab93f5c6..3b2712f13 100644
--- a/app/lib/LevelLoader.coffee
+++ b/app/lib/LevelLoader.coffee
@@ -420,7 +420,8 @@ module.exports = class LevelLoader extends CocoClass
resource.markLoaded() if resource.spriteSheetKeys.length is 0
denormalizeSession: ->
- return if @headless or @sessionDenormalized or @spectateMode or @sessionless or me.isSessionless()
+ return if @sessionDenormalized or @spectateMode or @sessionless or me.isSessionless()
+ return if @headless and not @level.isType('web-dev')
# This is a way (the way?) PUT /db/level.sessions/undefined was happening
# See commit c242317d9
return if not @session.id
diff --git a/app/locale/en.coffee b/app/locale/en.coffee
index 63d062364..c4706cb8c 100644
--- a/app/locale/en.coffee
+++ b/app/locale/en.coffee
@@ -450,8 +450,6 @@
incomplete: "Incomplete"
timed_out: "Ran out of time"
failing: "Failing"
- control_bar_multiplayer: "Multiplayer"
- control_bar_join_game: "Join Game"
reload: "Reload"
reload_title: "Reload All Code?"
reload_really: "Are you sure you want to reload this level back to the beginning?"
@@ -480,8 +478,7 @@
tome_cast_button_running: "Running"
tome_cast_button_ran: "Ran"
tome_submit_button: "Submit"
- tome_reload_method: "Reload original code for this method" # Title text for individual method reload button.
- tome_see_all_methods: "See all methods you can edit" # Title text for method list selector (shown when there are multiple programmable methods).
+ tome_reload_method: "Reload original code to restart the level" # {change}
tome_available_spells: "Available Spells"
tome_your_skills: "Your Skills"
tome_current_method: "Current Method"
@@ -1475,6 +1472,29 @@
status_not_enrolled: "Not Enrolled"
status_enrolled: "Expires on {{date}}"
select_all: "Select All"
+ projects: "Projects"
+
+ sharing:
+ game: "Game"
+ webpage: "Webpage"
+ share_game: "Share This Game"
+ share_web: "Share This Webpage"
+ victory_share_prefix: "Share this link to invite your friends & family to"
+ victory_share_game: "play your game level"
+ victory_share_web: "view your webpage"
+ victory_share_suffix: "."
+ victory_course_share_prefix: "This link will let your friends & family"
+ victory_course_share_game: "play the game"
+ victory_course_share_web: "view the webpage"
+ victory_course_share_suffix: "you just created."
+ copy_url: "Copy URL"
+
+ game_dev:
+ creator: "Creator"
+
+ web_dev:
+ image_gallery_title: "Image Gallery"
+ image_gallery_description: "Copy these images into your webpage, or find your own image URLs online."
classes:
archmage_title: "Archmage"
diff --git a/app/schemas/schemas.coffee b/app/schemas/schemas.coffee
index 8dcd9bfb9..9df53b58a 100644
--- a/app/schemas/schemas.coffee
+++ b/app/schemas/schemas.coffee
@@ -264,4 +264,12 @@ me.concept = me.shortString enum: [
'basic_html'
'basic_css'
'basic_web_scripting'
+ 'intermediate_html'
+ 'intermediate_css'
+ 'intermediate_web_scripting'
+ 'advanced_html'
+ 'advanced_css'
+ 'advanced_web_scripting'
+ 'jquery'
+ 'bootstrap'
]
diff --git a/app/styles/play/level/play-game-dev-level-view.sass b/app/styles/play/level/play-game-dev-level-view.sass
index 9b8006bd2..c14a38add 100644
--- a/app/styles/play/level/play-game-dev-level-view.sass
+++ b/app/styles/play/level/play-game-dev-level-view.sass
@@ -17,3 +17,6 @@
canvas#webgl-surface, canvas#normal-surface
display: block
z-index: 2
+
+ #play-btn
+ text-transform: uppercase
diff --git a/app/styles/play/level/web-surface-view.sass b/app/styles/play/level/web-surface-view.sass
index ddfed6497..e66a99e87 100644
--- a/app/styles/play/level/web-surface-view.sass
+++ b/app/styles/play/level/web-surface-view.sass
@@ -4,50 +4,3 @@
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
-//
diff --git a/app/templates/courses/course-details.jade b/app/templates/courses/course-details.jade
index 78953cc33..2ee0f69b0 100644
--- a/app/templates/courses/course-details.jade
+++ b/app/templates/courses/course-details.jade
@@ -104,8 +104,8 @@ block content
tr
td
if previousLevelCompleted || view.teacherMode || !passedLastCompletedLevel || levelStatus
- - 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'))
+ - var i18nTag = level.isType('course-ladder') ? 'play.compete' : 'home.play';
+ button.btn.btn-success.btn-play-level(data-level-slug=level.get('slug'), data-i18n=i18nTag, data-level-id=level.get('original'))
if level.get('shareable')
- var levelOriginal = level.get('original');
- var session = view.levelSessions.find(function(session) { return session.get('level').original === levelOriginal });
@@ -113,14 +113,14 @@ block content
- var url = '/play/' + level.get('type') + '-level/' + level.get('slug') + '/' + session.id + '?course=' + view.courseID;
a.btn.btn-warning.btn-view-project-level(href=url)
if level.isType('game-dev')
- span(data-i18n='') Game
+ span(data-i18n='sharing.game')
else
- span(data-i18n='') Webpage
+ span(data-i18n='sharing.webpage')
td
if view.userLevelStateMap[me.id]
div= view.userLevelStateMap[me.id][level.get('original')]
td #{level.get('practice') ? 'practice' : 'required'}
- td #{levelNumber}. #{level.get('name').replace('Course: ', '')}
+ td #{levelNumber}. #{i18n(level.attributes, 'name').replace('Course: ', '')}
td
if view.levelConceptMap[level.get('original')]
each concept in view.course.get('concepts')
diff --git a/app/templates/courses/teacher-class-view.jade b/app/templates/courses/teacher-class-view.jade
index 5085fa80d..699dc22c1 100644
--- a/app/templates/courses/teacher-class-view.jade
+++ b/app/templates/courses/teacher-class-view.jade
@@ -125,7 +125,7 @@ block content
.tab-spacer
li(class=(activeTab === "#student-projects-tab" ? 'active' : ''))
a.course-progress-tab-btn(href='#student-projects-tab')
- .small-details.text-center(data-i18n='') Projects
+ .small-details.text-center(data-i18n='teacher.projects')
.tab-filler
.tab-content
@@ -156,11 +156,10 @@ mixin breadcrumbs
mixin longLevelName(data)
if data
div.level-name
- span.spr Course
- span= data.courseNumber
- span.spr , Level
- span= data.levelNumber
- span.spr :
+ span(data-i18n="courses.course")
+ span= ' ' + data.courseNumber + ', '
+ span(data-i18n="play_level.level")
+ span= ' ' + data.levelNumber + ': '
span= data.levelName
else
div.level-name(data-i18n='teacher.not_applicable')
@@ -345,7 +344,7 @@ mixin studentCourseProgressDot(progress, levelsTotal, level, label)
mixin allStudentsLevelProgressDot(progress, level, levelNumber)
- dotClass = progress.completed ? 'forest' : (progress.started ? 'gold' : '');
- - levelName = level.get('name')
+ - levelName = i18n(level.attributes, 'name')
- context = _.merge(progress, { levelName: levelName, levelNumber: levelNumber, numStudents: view.students.length })
.progress-dot.level-progress-dot(class=dotClass, data-html='true', data-title=view.allStudentsLevelProgressDotTemplate(context))
+progressDotLabel(levelNumber)
@@ -353,7 +352,7 @@ mixin allStudentsLevelProgressDot(progress, level, levelNumber)
mixin studentLevelProgressDot(progress, level, levelNumber)
//- TODO: Refactor with TeacherClassesView jade
- dotClass = progress.completed ? 'forest' : (progress.started ? 'gold' : '');
- - levelName = level.get('name')
+ - levelName = i18n(level.attributes, 'name')
- context = _.merge(progress, { levelName: levelName, levelNumber: levelNumber, moment: moment })
.progress-dot.level-progress-dot(class=dotClass, data-html='true', data-title=view.singleStudentLevelProgressDotTemplate(context))
+progressDotLabel(levelNumber)
@@ -465,7 +464,7 @@ mixin studentProjectsRow(student)
mixin studentProjectLink(progress, level, levelNumber, course)
- var colorClass = progress.completed ? 'btn-primary' : (progress.started ? 'btn-warning' : 'btn-primary');
- - var levelName = level.get('name')
+ - var levelName = i18n(level.attributes, 'name')
- var context = _.merge(progress, { levelName: levelName, levelNumber: levelNumber, moment: moment })
- var title = view.singleStudentLevelProgressDotTemplate(context);
if context.session
diff --git a/app/templates/play/level/modal/hero-victory-modal.jade b/app/templates/play/level/modal/hero-victory-modal.jade
index 865198358..c1b651c82 100644
--- a/app/templates/play/level/modal/hero-victory-modal.jade
+++ b/app/templates/play/level/modal/hero-victory-modal.jade
@@ -111,19 +111,20 @@ block modal-footer-content
if view.shareURL
#share-level-container
span.share-level-label
- span(data-i18n='') Share this link to invite your friends & family to
+ span(data-i18n='sharing.victory_share_prefix') Share this link to invite your friends & family to
span= ' '
a(href=view.shareURL, target='_blank')
if view.level.isType('game-dev')
- span(data-i18n='') play your game level
+ span(data-i18n='sharing.victory_share_game') play your game level
else
- span(data-i18n='') view your webpage
+ span(data-i18n='sharing.victory_share_web') view your webpage
+ span(data-i18n='sharing.victory_share_suffix') .
.row
.col-sm-9
input.text-h4.semibold.form-control.input-md#share-level-input(value=view.shareURL)
.col-sm-3
button#share-level-btn.btn.btn-md.btn-success.btn-illustrated
- span(data-i18n='') Copy URL
+ span(data-i18n='sharing.copy_url') Copy URL
if me.get('anonymous')
.sign-up-poke.hide
diff --git a/app/templates/play/level/modal/image-gallery-modal.jade b/app/templates/play/level/modal/image-gallery-modal.jade
index b21862dc6..7899ea8c2 100644
--- a/app/templates/play/level/modal/image-gallery-modal.jade
+++ b/app/templates/play/level/modal/image-gallery-modal.jade
@@ -1,8 +1,8 @@
extends /templates/core/modal-base-flat
block modal-header-content
- h3(data-i18n='') Image Gallery
- | Copy these images into your webpage, or find your own image URLs online.
+ h3(data-i18n="web_dev.image_gallery_title")
+ span(data-i18n="web_dev.image_gallery_description")
block modal-body-content
dl.dl-horizontal
diff --git a/app/templates/play/level/modal/progress-view.jade b/app/templates/play/level/modal/progress-view.jade
index 82edb9a6e..35f331c8b 100644
--- a/app/templates/play/level/modal/progress-view.jade
+++ b/app/templates/play/level/modal/progress-view.jade
@@ -44,25 +44,25 @@
.well.well-sm.well-parchment
h3.text-uppercase
if view.level.isType('game-dev')
- span(data-i18n='') Share This Game
+ span(data-i18n='sharing.share_game')
else
- span(data-i18n='') Share This Webpage
+ span(data-i18n='sharing.share_web')
p
- span(data-i18n='') This link will let your friends & family
+ span(data-i18n='sharing.victory_course_share_prefix')
span= ' '
a(href=view.shareURL, target='_blank')
if view.level.isType('game-dev')
- span(data-i18n='') play the game
+ span(data-i18n='sharing.victory_course_share_game')
else
- span(data-i18n='') view the webpage
+ span(data-i18n='sharing.victory_course_share_web')
span= ' '
- span(data-i18n='') you just created.
+ span(data-i18n='sharing.victory_course_share_suffix')
.row
.col-sm-9
input.text-h4.semibold.form-control.input-lg#share-level-input(value=view.shareURL)
.col-sm-3
button#share-level-btn.btn.btn-lg.btn-success.btn-illustrated
- span(data-i18n='') Copy URL
+ span(data-i18n='sharing.copy_url')
.row
.col-sm-5.col-sm-offset-2
diff --git a/app/templates/play/level/play-game-dev-level-view.jade b/app/templates/play/level/play-game-dev-level-view.jade
index 928cd1c12..c4b80c67e 100644
--- a/app/templates/play/level/play-game-dev-level-view.jade
+++ b/app/templates/play/level/play-game-dev-level-view.jade
@@ -10,7 +10,7 @@
.alert.alert-danger= view.state.get('errorMessage')
else if view.state.get('loading')
- h1.m-y-1 Loading...
+ h1.m-y-1(data-i18n="common.loading")
.progress
.progress-bar(style="width: #{view.state.get('progress')}")
@@ -18,17 +18,21 @@
h1.m-y-1 Info
ul
li
- b Level Name:
- | #{view.level.get('name')}
+ b
+ span(data-i18n="play_level.level")
+ span= ': '
+ | #{view.level.get('name')}
li
- b Creator:
- | #{view.session.get('creatorName')}
+ b
+ span(data-i18n="game_dev.creator")
+ span= ': '
+ | #{view.session.get('creatorName')}
- var playing = view.state.get('playing')
.m-y-3
if playing
- button#play-btn.btn.btn-lg.btn-burgandy RESTART
+ button#play-btn.btn.btn-lg.btn-burgandy(data-i18n="play_level.restart")
else
- button#play-btn.btn.btn-lg.btn-navy PLAY
+ button#play-btn.btn.btn-lg.btn-navy(data-i18n="common.play")
diff --git a/app/templates/play/level/play-web-dev-level-view.jade b/app/templates/play/level/play-web-dev-level-view.jade
index 9d180c707..4e87873de 100644
--- a/app/templates/play/level/play-web-dev-level-view.jade
+++ b/app/templates/play/level/play-web-dev-level-view.jade
@@ -6,5 +6,6 @@
else
h1
- span Creator:
- | #{view.session.get('creatorName')}
+ span(data-i18n="game_dev.creator")
+ span= ': '
+ | #{view.session.get('creatorName')}
diff --git a/app/templates/play/level/tome/spell-top-bar-view.jade b/app/templates/play/level/tome/spell-top-bar-view.jade
index f6a8faa2c..2af252e1e 100644
--- a/app/templates/play/level/tome/spell-top-bar-view.jade
+++ b/app/templates/play/level/tome/spell-top-bar-view.jade
@@ -4,9 +4,9 @@
.hinge.hinge-3
.spell-tool-buttons
- .btn.btn-small.btn-illustrated.btn-warning.reload-code(data-i18n="[title]play_level.tome_reload_method", title="Reload original code for this method")
+ .btn.btn-small.btn-illustrated.btn-warning.reload-code(data-i18n="[title]play_level.tome_reload_method")
.glyphicon.glyphicon-repeat
- span.spl(data-i18n="play_level.reload") Reload
+ span.spl(data-i18n="play_level.restart")
if me.level() >= 15
.btn.btn-small.btn-illustrated.fullscreen-code(title=maximizeShortcutVerbose)
@@ -23,15 +23,15 @@
if view.options.level.isType('web-dev')
.btn.btn-small.btn-illustrated.image-gallery-button
- span(data-i18n='') Image Gallery
+ span(data-i18n='web_dev.image_gallery_title')
if view.options.level.get('shareable')
- var url = '/play/' + view.options.level.get('type') + '-level/' + view.options.level.get('slug') + '/' + view.options.session.id;
- if (view.options.courseID) url += '?course=' + view.options.courseID;
a.btn.btn-small.btn-illustrated(href=url)
if view.options.level.isType('game-dev')
- span(data-i18n='') Game
+ span(data-i18n='sharing.game')
else
- span(data-i18n='') Webpage
+ span(data-i18n='sharing.webpage')
.clearfix
diff --git a/app/views/courses/CourseDetailsView.coffee b/app/views/courses/CourseDetailsView.coffee
index 91d10c1f6..2f7e4c4f4 100644
--- a/app/views/courses/CourseDetailsView.coffee
+++ b/app/views/courses/CourseDetailsView.coffee
@@ -52,7 +52,7 @@ module.exports = class CourseDetailsView extends RootView
@supermodel.trackRequest(@classroom.fetch())
levelsLoaded = @supermodel.trackRequest(@levels.fetchForClassroomAndCourse(classroomID, @courseID, {
- data: { project: 'concepts,practice,type,slug,name,original,description,shareable' }
+ data: { project: 'concepts,practice,type,slug,name,original,description,shareable,i18n' }
}))
@supermodel.trackRequest($.when(levelsLoaded, sessionsLoaded).then(=>
diff --git a/app/views/courses/TeacherClassView.coffee b/app/views/courses/TeacherClassView.coffee
index 059166a5c..092b5d695 100644
--- a/app/views/courses/TeacherClassView.coffee
+++ b/app/views/courses/TeacherClassView.coffee
@@ -43,7 +43,7 @@ module.exports = class TeacherClassView extends RootView
'click .student-checkbox': 'onClickStudentCheckbox'
'keyup #student-search': 'onKeyPressStudentSearch'
'change .course-select, .bulk-course-select': 'onChangeCourseSelect'
-
+
getInitialState: ->
{
sortAttribute: 'name'
@@ -72,21 +72,21 @@ module.exports = class TeacherClassView extends RootView
@singleStudentCourseProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-single-student-course'
@singleStudentLevelProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-single-student-level'
@allStudentsLevelProgressDotTemplate = require 'templates/teachers/hovers/progress-dot-all-students-single-level'
-
+
@debouncedRender = _.debounce @render
-
+
@state = new State(@getInitialState())
@updateHash @state.get('activeTab') # TODO: Don't push to URL history (maybe don't use url fragment for default tab)
-
+
@classroom = new Classroom({ _id: classroomID })
@supermodel.trackRequest @classroom.fetch()
@onKeyPressStudentSearch = _.debounce(@onKeyPressStudentSearch, 200)
-
+
@students = new Users()
@listenTo @classroom, 'sync', ->
jqxhrs = @students.fetchForClassroom(@classroom, removeDeleted: true)
@supermodel.trackRequests jqxhrs
-
+
@classroom.sessions = new LevelSessions()
requests = @classroom.sessions.fetchForAllClassroomMembers(@classroom)
@supermodel.trackRequests(requests)
@@ -96,7 +96,7 @@ module.exports = class TeacherClassView extends RootView
value = @state.get('sortValue')
if value is 'name'
return (if student1.broadName().toLowerCase() < student2.broadName().toLowerCase() then -dir else dir)
-
+
if value is 'progress'
# TODO: I would like for this to be in the Level model,
# but it doesn't know about its own courseNumber.
@@ -105,7 +105,7 @@ module.exports = class TeacherClassView extends RootView
return -dir if not level1
return dir if not level2
return dir * (level1.courseNumber - level2.courseNumber or level1.levelNumber - level2.levelNumber)
-
+
if value is 'status'
statusMap = { expired: 0, 'not-enrolled': 1, enrolled: 2 }
diff = statusMap[student1.prepaidStatus()] - statusMap[student2.prepaidStatus()]
@@ -119,7 +119,7 @@ module.exports = class TeacherClassView extends RootView
@supermodel.trackRequest @courseInstances.fetchForClassroom(classroomID)
@levels = new Levels()
- @supermodel.trackRequest @levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts,practice,shareable'}})
+ @supermodel.trackRequest @levels.fetchForClassroom(classroomID, {data: {project: 'original,concepts,practice,shareable,i18n'}})
@attachMediatorEvents()
window.tracker?.trackEvent 'Teachers Class Loaded', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
@@ -160,11 +160,11 @@ module.exports = class TeacherClassView extends RootView
course.instance = @courseInstances.findWhere({ courseID: course.id, classroomID: @classroom.id })
course.members = course.instance?.get('members') or []
null
-
+
onLoaded: ->
@removeDeletedStudents() # TODO: Move this to mediator listeners? For both classroom and students?
@calculateProgressAndLevels()
-
+
# render callback setup
@listenTo @courseInstances, 'sync change update', @debouncedRender
@listenTo @state, 'sync change', ->
@@ -192,14 +192,14 @@ module.exports = class TeacherClassView extends RootView
# TODO: this is a weird hack
studentsStub = new Users([ student ])
student.latestCompleteLevel = helper.calculateLatestComplete(@classroom, @courses, @courseInstances, studentsStub)
-
+
earliestIncompleteLevel = helper.calculateEarliestIncomplete(@classroom, @courses, @courseInstances, @students)
latestCompleteLevel = helper.calculateLatestComplete(@classroom, @courses, @courseInstances, @students)
-
+
classroomsStub = new Classrooms([ @classroom ])
progressData = helper.calculateAllProgress(classroomsStub, @courses, @courseInstances, @students)
# conceptData: helper.calculateConceptsCovered(classroomsStub, @courses, @campaigns, @courseInstances, @students)
-
+
@state.set {
earliestIncompleteLevel
latestCompleteLevel
@@ -212,7 +212,7 @@ module.exports = class TeacherClassView extends RootView
hash = $(e.target).closest('a').attr('href')
@updateHash(hash)
@state.set activeTab: hash
-
+
updateHash: (hash) ->
return if application.testing
window.location.hash = hash
@@ -230,7 +230,7 @@ module.exports = class TeacherClassView extends RootView
onClickUnarchive: ->
window.tracker?.trackEvent 'Teachers Class Unarchive', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
@classroom.save { archived: false }
-
+
onClickEditClassroom: (e) ->
window.tracker?.trackEvent 'Teachers Class Edit Class Started', category: 'Teachers', classroomID: @classroom.id, ['Mixpanel']
classroom = @classroom
@@ -455,7 +455,7 @@ module.exports = class TeacherClassView extends RootView
enrolledUsers = @students.filter (user) -> user.isEnrolled()
stats.enrolledUsers = _.size(enrolledUsers)
-
+
return stats
studentStatusString: (student) ->
diff --git a/app/views/play/level/PlayWebDevLevelView.coffee b/app/views/play/level/PlayWebDevLevelView.coffee
index 660a3b58d..40d99e3f7 100644
--- a/app/views/play/level/PlayWebDevLevelView.coffee
+++ b/app/views/play/level/PlayWebDevLevelView.coffee
@@ -8,9 +8,6 @@ module.exports = class PlayWebDevLevelView extends RootView
id: 'play-web-dev-level-view'
template: require 'templates/play/level/play-web-dev-level-view'
-# events:
-# 'click #play-btn': 'onClickPlayButton'
-
initialize: (@options, @levelID, @sessionID) ->
@courseID = @getQueryVariable 'course'
@level = @supermodel.loadModel(new Level _id: @levelID).model
@@ -21,7 +18,7 @@ module.exports = class PlayWebDevLevelView extends RootView
@insertSubView @webSurface = new WebSurfaceView {level: @level}
Backbone.Mediator.publish 'tome:html-updated', html: @getHTML() ? 'Player has no HTML
', create: true
@$el.find('#info-bar').delay(4000).fadeOut(2000)
- $('body').css('overflow', 'hidden')
+ $('body').css('overflow', 'hidden') # Don't show tiny scroll bar from our minimal additions to the iframe
showError: (jqxhr) ->
$('h1').text jqxhr.statusText
@@ -34,5 +31,5 @@ module.exports = class PlayWebDevLevelView extends RootView
destroy: ->
@webSurface?.destroy()
- $('body').css('overflow', 'initial')
+ $('body').css('overflow', 'initial') # Recover from our modifications to body overflow before we leave
super()
diff --git a/app/views/play/level/WebSurfaceView.coffee b/app/views/play/level/WebSurfaceView.coffee
index fa3e374be..28712d19e 100644
--- a/app/views/play/level/WebSurfaceView.coffee
+++ b/app/views/play/level/WebSurfaceView.coffee
@@ -1,5 +1,4 @@
CocoView = require 'views/core/CocoView'
-State = require 'models/State'
template = require 'templates/play/level/web-surface-view'
module.exports = class WebSurfaceView extends CocoView
@@ -10,8 +9,6 @@ module.exports = class WebSurfaceView extends CocoView
'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)
@@ -21,7 +18,6 @@ module.exports = class WebSurfaceView extends CocoView
@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
diff --git a/app/views/play/level/modal/HeroVictoryModal.coffee b/app/views/play/level/modal/HeroVictoryModal.coffee
index 2b30a5660..3e7ce32a6 100644
--- a/app/views/play/level/modal/HeroVictoryModal.coffee
+++ b/app/views/play/level/modal/HeroVictoryModal.coffee
@@ -157,7 +157,6 @@ module.exports = class HeroVictoryModal extends ModalView
getRenderData: ->
c = super()
c.levelName = utils.i18n @level.attributes, 'name'
- # 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'))
@@ -226,7 +225,7 @@ module.exports = class HeroVictoryModal extends ModalView
afterRender: ->
super()
- @$el.toggleClass 'with-achievements', @level.isType('hero', 'hero-ladder', 'game-dev', 'web-dev') # TODO: support game-dev, web-dev
+ @$el.toggleClass 'with-achievements', @level.isType('hero', 'hero-ladder', 'game-dev', 'web-dev')
return unless @supermodel.finished()
@playSelectionSound hero, true for original, hero of @thangTypes # Preload them
@updateSavingProgressStatus()
@@ -236,7 +235,7 @@ module.exports = class HeroVictoryModal extends ModalView
@insertSubView @ladderSubmissionView, @$el.find('.ladder-submission-view')
initializeAnimations: ->
- return @endSequentialAnimations() unless @level.isType('hero', 'hero-ladder', 'game-dev', 'web-dev') # TODO: support game-dev, web-dev
+ return @endSequentialAnimations() unless @level.isType('hero', 'hero-ladder', 'game-dev', 'web-dev')
@updateXPBars 0
#playVictorySound = => @playSound 'victory-title-appear' # TODO: actually add this
@$el.find('#victory-header').delay(250).queue(->
@@ -267,7 +266,7 @@ module.exports = class HeroVictoryModal extends ModalView
beginSequentialAnimations: ->
return if @destroyed
- return unless @level.isType('hero', 'hero-ladder', 'game-dev', 'web-dev') # TODO: support game-dev, web-dev
+ return unless @level.isType('hero', 'hero-ladder', 'game-dev', 'web-dev')
@sequentialAnimatedPanels = _.map(@animatedPanels.find('.reward-panel'), (panel) -> {
number: $(panel).data('number')
previousNumber: $(panel).data('previous-number')
@@ -417,7 +416,7 @@ 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.isType('course', 'game-dev', 'web-dev') and nextLevel = @level.get('nextLevel') and not returnToCourse # TODO: support game-dev and web-dev
+ if @level.isType('course', 'game-dev', 'web-dev') and nextLevel = @level.get('nextLevel') and not returnToCourse
# need to do something more complicated to load its slug
console.log 'have @nextLevel', @nextLevel, 'from nextLevel', nextLevel
link = "/play/level/#{@nextLevel.get('slug')}"
diff --git a/app/views/play/level/tome/DocFormatter.coffee b/app/views/play/level/tome/DocFormatter.coffee
index cc890e5bf..b09d7092b 100644
--- a/app/views/play/level/tome/DocFormatter.coffee
+++ b/app/views/play/level/tome/DocFormatter.coffee
@@ -42,7 +42,7 @@ module.exports = class DocFormatter
@fillOutDoc()
fillOutDoc: ->
- # TODO: figure out how to do html docs for web-dev levels
+ # TODO: figure out better ways to format html/css/scripting docs for web-dev levels
if _.isString @doc
@doc = name: @doc, type: typeof @options.thang[@doc]
if @options.isSnippet
diff --git a/app/views/play/level/tome/Spell.coffee b/app/views/play/level/tome/Spell.coffee
index 6bc7bc265..ddd1f187c 100644
--- a/app/views/play/level/tome/Spell.coffee
+++ b/app/views/play/level/tome/Spell.coffee
@@ -72,11 +72,12 @@ module.exports = class Spell
@originalSource = @addPicoCTFProblem() if window.serverConfig.picoCTF
if @level.isType('web-dev')
+ # Pull apart the structural wrapper code and the player code, remember the wrapper code, and strip indentation on player code.
playerCode = @originalSource.match(/\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 /[\s\S]*<\/playercode>/, '☃'
+ @wrapperCode = @originalSource.replace /[\s\S]*<\/playercode>/, '☃' # ☃ serves as placeholder for constructHTML
@originalSource = playerCode
# Translate comments chosen spoken language.
diff --git a/app/views/play/level/tome/TomeView.coffee b/app/views/play/level/tome/TomeView.coffee
index b9fa26e77..d35bbbb4c 100644
--- a/app/views/play/level/tome/TomeView.coffee
+++ b/app/views/play/level/tome/TomeView.coffee
@@ -210,7 +210,6 @@ module.exports = class TomeView extends CocoView
@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
diff --git a/app/views/play/menu/GuideView.coffee b/app/views/play/menu/GuideView.coffee
index e64c2c2a0..fcd794117 100644
--- a/app/views/play/menu/GuideView.coffee
+++ b/app/views/play/menu/GuideView.coffee
@@ -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.isType('course', 'course-ladder') # TODO: figure this out for game-dev, web-dev levels
+ @isCourseLevel = options.level.isType('course', 'course-ladder')
@helpVideos = if @isCourseLevel then [] else options.level.get('helpVideos') ? []
@trackedHelpVideoStart = @trackedHelpVideoFinish = false
# A/B Testing video tutorial styles
From f94cc2ec1f4e79cc372f868e5d505ab36a707fe2 Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Sun, 17 Jul 2016 01:12:58 -0700
Subject: [PATCH 51/58] Fix CS1, CS2, GD1, WD2, CS3, etc. labeling in
TeacherClassesView, too
---
app/lib/coursesHelper.coffee | 10 ++++++++++
app/templates/courses/teacher-class-view.jade | 2 +-
app/templates/courses/teacher-classes-view.jade | 17 ++++++++---------
app/views/courses/TeacherClassView.coffee | 14 ++------------
app/views/courses/TeacherClassesView.coffee | 1 +
5 files changed, 22 insertions(+), 22 deletions(-)
diff --git a/app/lib/coursesHelper.coffee b/app/lib/coursesHelper.coffee
index c50873c62..a6186ada5 100644
--- a/app/lib/coursesHelper.coffee
+++ b/app/lib/coursesHelper.coffee
@@ -195,6 +195,16 @@ module.exports =
_.assign(progressData, progressMixin)
return progressData
+ courseLabelsArray: (courses) ->
+ labels = []
+ courseLabelIndexes = CS: 0, GD: 0, WD: 0
+ for course in courses
+ acronym = switch
+ when /game-dev/.test(course.get('slug')) then 'GD'
+ when /web-dev/.test(course.get('slug')) then 'WD'
+ else 'CS'
+ labels.push acronym + ++courseLabelIndexes[acronym]
+ labels
progressMixin =
get: (options={}) ->
diff --git a/app/templates/courses/teacher-class-view.jade b/app/templates/courses/teacher-class-view.jade
index 699dc22c1..d1581640a 100644
--- a/app/templates/courses/teacher-class-view.jade
+++ b/app/templates/courses/teacher-class-view.jade
@@ -229,7 +229,7 @@ mixin studentRow(student)
td
if state.get('progressData')
- var courses = view.classroom.get('courses').map(function(c) { return view.courses.get(c._id); });
- - var courseLabelsArray = view.courseLabelsArray(courses);
+ - var courseLabelsArray = view.helper.courseLabelsArray(courses);
each trimCourse, index in view.classroom.get('courses')
- var course = view.courses.get(trimCourse._id);
- var instance = view.courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id })
diff --git a/app/templates/courses/teacher-classes-view.jade b/app/templates/courses/teacher-classes-view.jade
index 82e11e3b9..ee8839edd 100644
--- a/app/templates/courses/teacher-classes-view.jade
+++ b/app/templates/courses/teacher-classes-view.jade
@@ -76,10 +76,13 @@ mixin classRow(classroom)
if classroom.get('members').length == 0
+addStudentsButton(classroom)
else
+ - var courses = classroom.get('courses').map(function(c) { return view.courses.get(c._id); });
+ - var courseLabelsArray = view.helper.courseLabelsArray(courses);
each trimCourse, index in classroom.get('courses') || []
- var course = view.courses.get(trimCourse._id);
if view.courseInstances.findWhere({ classroomID: classroom.id, courseID: course.id })
- +progressDot(classroom, course, index)
+ - var label = courseLabelsArray[index];
+ +progressDot(classroom, course, label)
.view-class-arrow.col-xs-1
a.view-class-arrow-inner.glyphicon.glyphicon-chevron-right.view-class-btn(data-classroom-id=classroom.id data-event-action="Teachers Classes View Class Chevron")
@@ -99,8 +102,7 @@ mixin createClassButton
a.create-classroom-btn.btn.btn-lg.btn-primary(data-i18n='teacher.create_new_class')
| Create a New Class
-mixin progressDot(classroom, course, index)
- //- TODO: Give classes abbreviations instead of using index?
+mixin progressDot(classroom, course, label)
//- TODO: inefficient. Cache this in the view?
- courseInstance = view.courseInstances.findWhere({ courseID: course.id, classroomID: classroom.id })
- var total = classroom.get('members').length
@@ -113,14 +115,11 @@ mixin progressDot(classroom, course, index)
- dotClass = complete === total ? 'forest' : started ? 'gold' : '';
- var progressDotContext = {total: total, complete: complete};
.progress-dot(class=dotClass, data-title=view.progressDotTemplate(progressDotContext))
- +progressDotLabel(index)
+ +progressDotLabel(label)
-mixin progressDotLabel(index)
+mixin progressDotLabel(label)
.dot-label
- .text-h6
- | CS
- span
- = index + 1
+ .text-h6= label
mixin archivedClassRow(classroom)
.class.row
diff --git a/app/views/courses/TeacherClassView.coffee b/app/views/courses/TeacherClassView.coffee
index 092b5d695..78444dbc1 100644
--- a/app/views/courses/TeacherClassView.coffee
+++ b/app/views/courses/TeacherClassView.coffee
@@ -23,6 +23,7 @@ CourseInstances = require 'collections/CourseInstances'
module.exports = class TeacherClassView extends RootView
id: 'teacher-class-view'
template: template
+ helper: helper
events:
'click .nav-tabs a': 'onClickNavTabLink'
@@ -322,7 +323,7 @@ module.exports = class TeacherClassView extends RootView
courseLabels = ""
courseOrder = []
courses = (@courses.get(c._id) for c in @classroom.get('courses'))
- courseLabelsArray = @courseLabelsArray courses
+ courseLabelsArray = helper.courseLabelsArray courses
for course, index in courses
courseLabels += "#{courseLabelsArray[index]} Playtime,"
courseOrder.push(course.id)
@@ -466,14 +467,3 @@ module.exports = class TeacherClassView extends RootView
when 'enrolled' then (if expires then $.i18n.t('teacher.status_enrolled') else '-')
when 'expired' then $.i18n.t('teacher.status_expired')
return string.replace('{{date}}', moment(expires).utc().format('l'))
-
- courseLabelsArray: (courses) ->
- labels = []
- courseLabelIndexes = CS: 0, GD: 0, WD: 0
- for course in courses
- acronym = switch
- when /game-dev/.test(course.get('slug')) then 'GD'
- when /web-dev/.test(course.get('slug')) then 'WD'
- else 'CS'
- labels.push acronym + ++courseLabelIndexes[acronym]
- labels
diff --git a/app/views/courses/TeacherClassesView.coffee b/app/views/courses/TeacherClassesView.coffee
index b281a2ce0..5dc0c77ee 100644
--- a/app/views/courses/TeacherClassesView.coffee
+++ b/app/views/courses/TeacherClassesView.coffee
@@ -17,6 +17,7 @@ helper = require 'lib/coursesHelper'
module.exports = class TeacherClassesView extends RootView
id: 'teacher-classes-view'
template: template
+ helper: helper
events:
'click .edit-classroom': 'onClickEditClassroom'
From cb1021e0131bd47dbeeb4b38e78259e5975415d9 Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Mon, 18 Jul 2016 22:27:20 -0700
Subject: [PATCH 52/58] Fix typo in game-dev-2 ordering
---
server/models/Course.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/server/models/Course.coffee b/server/models/Course.coffee
index cdec72b8d..bab22640f 100644
--- a/server/models/Course.coffee
+++ b/server/models/Course.coffee
@@ -20,7 +20,7 @@ CourseSchema.statics.sortCourses = (courses) ->
'game-dev-1'
'web-dev-1'
'computer-science-3'
- 'game-dev-1'
+ 'game-dev-2'
'web-dev-2'
'computer-science-4'
'game-dev-3'
From 58284dff338efe328c2a38af8f3df2581b8abd2f Mon Sep 17 00:00:00 2001
From: phoenixeliot
Date: Tue, 19 Jul 2016 11:29:57 -0700
Subject: [PATCH 53/58] Turn on Ace HTML worker for syntax errors
---
app/core/utils.coffee | 2 ++
app/views/play/level/tome/Problem.coffee | 1 +
app/views/play/level/tome/SpellPaletteEntryView.coffee | 1 +
app/views/play/level/tome/SpellView.coffee | 10 +++++++---
4 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/app/core/utils.coffee b/app/core/utils.coffee
index ca921ba82..166db035e 100644
--- a/app/core/utils.coffee
+++ b/app/core/utils.coffee
@@ -297,6 +297,8 @@ module.exports.aceEditModes = aceEditModes =
java: 'ace/mode/java'
html: 'ace/mode/html'
+# These ACEs are used for displaying code snippets statically, like in SpellPaletteEntryView popovers
+# and have short lifespans
module.exports.initializeACE = (el, codeLanguage) ->
contents = $(el).text().trim()
editor = ace.edit el
diff --git a/app/views/play/level/tome/Problem.coffee b/app/views/play/level/tome/Problem.coffee
index a42addd5d..9b5b22fa8 100644
--- a/app/views/play/level/tome/Problem.coffee
+++ b/app/views/play/level/tome/Problem.coffee
@@ -23,6 +23,7 @@ module.exports = class Problem
raw: text,
text: text,
type: @aetherProblem.level ? 'error'
+ createdBy: 'aether'
buildMarkerRange: ->
return unless @aetherProblem.range
diff --git a/app/views/play/level/tome/SpellPaletteEntryView.coffee b/app/views/play/level/tome/SpellPaletteEntryView.coffee
index c6332828c..1b400b673 100644
--- a/app/views/play/level/tome/SpellPaletteEntryView.coffee
+++ b/app/views/play/level/tome/SpellPaletteEntryView.coffee
@@ -53,6 +53,7 @@ module.exports = class SpellPaletteEntryView extends CocoView
oldEditor.destroy() for oldEditor in @aceEditors
@aceEditors = []
aceEditors = @aceEditors
+ # Initialize Ace for each popover code snippet
popover?.$tip?.find('.docs-ace').each ->
aceEditor = utils.initializeACE @, codeLanguage
aceEditors.push aceEditor
diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee
index bda7f943c..f288a12d1 100644
--- a/app/views/play/level/tome/SpellView.coffee
+++ b/app/views/play/level/tome/SpellView.coffee
@@ -21,6 +21,7 @@ module.exports = class SpellView extends CocoView
controlsEnabled: true
eventsSuppressed: true
writable: true
+ languagesThatUseWorkers: ['html']
keyBindings:
'default': null
@@ -78,6 +79,7 @@ module.exports = class SpellView extends CocoView
@lockDefaultCode()
_.defer @onAllLoaded # Needs to happen after the code generating this view is complete
+ # This ACE is used for the code editor, and is only instantiated once per level.
createACE: ->
# Test themes and settings here: http://ace.ajax.org/build/kitchen-sink.html
aceConfig = me.get('aceConfig') ? {}
@@ -85,7 +87,7 @@ module.exports = class SpellView extends CocoView
@ace = ace.edit @$el.find('.ace')[0]
@aceSession = @ace.getSession()
@aceDoc = @aceSession.getDocument()
- @aceSession.setUseWorker false
+ @aceSession.setUseWorker @spell.language in @languagesThatUseWorkers
@aceSession.setMode utils.aceEditModes[@spell.language]
@aceSession.setWrapLimitRange null
@aceSession.setUseWrapMode true
@@ -789,10 +791,12 @@ module.exports = class SpellView extends CocoView
else
finishUpdatingAether(aether)
+ # Clear annotations and highlights generated by Aether, but not by the ACE worker
clearAetherDisplay: ->
problem.destroy() for problem in @problems
@problems = []
- @aceSession.setAnnotations []
+ nonAetherAnnotations = _.reject @aceSession.getAnnotations(), (annotation) -> annotation.createdBy is 'aether'
+ @aceSession.setAnnotations nonAetherAnnotations
@highlightCurrentLine {} # This'll remove all highlights
displayAether: (aether, isCast=false) ->
@@ -800,7 +804,7 @@ module.exports = class SpellView extends CocoView
isCast = isCast or not _.isEmpty(aether.metrics) or _.some aether.getAllProblems(), {type: 'runtime'}
problem.destroy() for problem in @problems # Just in case another problem was added since clearAetherDisplay() ran.
@problems = []
- annotations = []
+ annotations = @aceSession.getAnnotations()
seenProblemKeys = {}
for aetherProblem, problemIndex in aether.getAllProblems()
continue if key = aetherProblem.userInfo?.key and key of seenProblemKeys
From 5bb7f243f5bd199bce64db7754890a15715e4bf5 Mon Sep 17 00:00:00 2001
From: Rob Blanckaert
Date: Tue, 19 Jul 2016 13:44:25 -0700
Subject: [PATCH 54/58] Update web-dev-listener.js
---
app/assets/javascripts/web-dev-listener.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/assets/javascripts/web-dev-listener.js b/app/assets/javascripts/web-dev-listener.js
index 3851eb316..9f3ee2ee8 100644
--- a/app/assets/javascripts/web-dev-listener.js
+++ b/app/assets/javascripts/web-dev-listener.js
@@ -11,6 +11,7 @@ var allowedOrigins = [
/http:\/\/localhost:3000/,
/http:\/\/direct\.codecombat\.com/,
/http:\/\/staging\.codecombat\.com/,
+ /http:\/\/next\.codecombat\.com/,
/http:\/\/.*codecombat-staging-codecombat\.runnableapp\.com/,
];
From 62813e41a3f4899b68d51a5013b9257fe0b43df9 Mon Sep 17 00:00:00 2001
From: phoenixeliot
Date: Wed, 20 Jul 2016 15:44:01 -0700
Subject: [PATCH 55/58] Extract styles and scripts
---
app/assets/javascripts/web-dev-listener.js | 50 +++++++++++++++-------
app/assets/web-dev-iframe.html | 6 +++
app/views/play/level/WebSurfaceView.coffee | 30 +++++++++++--
3 files changed, 67 insertions(+), 19 deletions(-)
diff --git a/app/assets/javascripts/web-dev-listener.js b/app/assets/javascripts/web-dev-listener.js
index 9f3ee2ee8..46251c231 100644
--- a/app/assets/javascripts/web-dev-listener.js
+++ b/app/assets/javascripts/web-dev-listener.js
@@ -2,8 +2,8 @@
window.addEventListener('message', receiveMessage, false);
-var concreteDOM;
-var virtualDOM;
+var concreteDom;
+var virtualDom;
var goalStates;
var allowedOrigins = [
@@ -19,7 +19,7 @@ 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);
+ allowed = allowed || pattern.test(origin);
});
if (!allowed) {
console.log('Ignoring message from bad origin:', origin);
@@ -30,14 +30,14 @@ function receiveMessage(event) {
var source = event.source;
switch (data.type) {
case 'create':
- create(data.dom);
+ create(_.pick(data, 'dom', 'styles', 'scripts'));
checkGoals(data.goals, source, origin);
break;
case 'update':
- if (virtualDOM)
- update(data.dom);
+ if (virtualDom)
+ update(_.pick(data, 'dom', 'styles', 'scripts'));
else
- create(data.dom);
+ create(_.pick(data, 'dom', 'styles', 'scripts'));
checkGoals(data.goals, source, origin);
break;
case 'log':
@@ -48,20 +48,40 @@ function receiveMessage(event) {
}
}
-function create(dom) {
- virtualDOM = dom;
- concreteDOM = deku.dom.create(dom);
+function create({ dom, styles, scripts }) {
+ virtualDom = dom;
+ virtualStyles = styles;
+ virtualScripts = scripts;
+ concreteDom = deku.dom.create(dom);
+ concreteStyles = styles.map(function(style){return deku.dom.create(style)});
+ concreteScripts = scripts.map(function(script){return deku.dom.create(script)});
// TODO: target the actual HTML tag and combine our initial structure for styles/scripts/tags with theirs
// TODO: :after elements don't seem to work? (:before do)
- $('body').empty().append(concreteDOM);
+ $('body').first().empty().append(concreteDom);
+ $('#player-styles').first().empty().append(concreteStyles);
+ $('#player-scripts').first().empty().append(concreteScripts);
}
-function update(dom) {
+function update({ dom, styles, scripts }) {
function dispatch() {} // Might want to do something here in the future
var context = {}; // Might want to use this to send shared state to every component
- var changes = deku.diff.diffNode(virtualDOM, dom);
- changes.reduce(deku.dom.update(dispatch, context), concreteDOM); // Rerender
- virtualDOM = dom;
+ var domChanges = deku.diff.diffNode(virtualDom, dom);
+ domChanges.reduce(deku.dom.update(dispatch, context), concreteDom); // Rerender
+
+ var scriptChanges = virtualScripts.map(function(virtualScript, index){
+ return deku.diff.diffNode(virtualScripts[index], scripts[index]);
+ });
+ scriptChanges.forEach(function(scriptChange, index){
+ scriptChange.reduce(deku.dom.update(dispatch, context), concreteStyles[index])
+ });
+
+ var styleChanges = virtualStyles.map(function(virtualStyle, index){
+ return deku.diff.diffNode(virtualStyles[index], styles[index]);
+ });
+ styleChanges.forEach(function(styleChange, index){
+ styleChange.reduce(deku.dom.update(dispatch, context), concreteStyles[index])
+ });
+ virtualDom = dom;
}
function checkGoals(goals, source, origin) {
diff --git a/app/assets/web-dev-iframe.html b/app/assets/web-dev-iframe.html
index c9019aea5..304578e33 100644
--- a/app/assets/web-dev-iframe.html
+++ b/app/assets/web-dev-iframe.html
@@ -30,6 +30,12 @@
+
+
+
+
Loading...
diff --git a/app/views/play/level/WebSurfaceView.coffee b/app/views/play/level/WebSurfaceView.coffee
index 28712d19e..f4dded462 100644
--- a/app/views/play/level/WebSurfaceView.coffee
+++ b/app/views/play/level/WebSurfaceView.coffee
@@ -31,18 +31,40 @@ module.exports = class WebSurfaceView extends CocoView
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
+ { virtualDom, styles, scripts } = @extractStylesAndScripts(@dekuify html)
+ messageType = if e.create or not @virtualDom then 'create' else 'update'
+ @iframe.contentWindow.postMessage {type: messageType, dom: virtualDom, styles, scripts, 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
+ elem.attribs = _.omit elem.attribs, (val, attr) -> attr.indexOf('<') > -1 # Deku chokes on `
`
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 ? []))
+
+ extractStylesAndScripts: (dekuTree) ->
+ #base case
+ if dekuTree.type is '#text'
+ return { virtualDom: dekuTree, styles: [], scripts: [] }
+ if dekuTree.type is 'style'
+ console.log 'Found a style: ', dekuTree
+ return { styles: [dekuTree], scripts: [] }
+ if dekuTree.type is 'script'
+ console.log 'Found a script: ', dekuTree
+ return { styles: [], scripts: [dekuTree] }
+ # recurse over children
+ childStyles = []
+ childScripts = []
+ dekuTree.children?.forEach (dekuChild, index) =>
+ { virtualDom, styles, scripts } = @extractStylesAndScripts(dekuChild)
+ dekuTree.children[index] = virtualDom
+ childStyles = childStyles.concat(styles)
+ childScripts = childScripts.concat(scripts)
+ dekuTree.children = _.filter dekuTree.children # Remove the nodes we extracted
+ return { virtualDom: dekuTree, scripts: childScripts, styles: childStyles }
onIframeMessage: (event) =>
origin = event.origin or event.originalEvent.origin
From f28c17c9bffe56fe32ff93c25416b99cc5d38da8 Mon Sep 17 00:00:00 2001
From: phoenixeliot
Date: Wed, 20 Jul 2016 15:47:20 -0700
Subject: [PATCH 56/58] Fix spacing to be consistent
---
app/assets/javascripts/web-dev-listener.js | 19 +++++++++----------
app/views/play/level/WebSurfaceView.coffee | 2 +-
2 files changed, 10 insertions(+), 11 deletions(-)
diff --git a/app/assets/javascripts/web-dev-listener.js b/app/assets/javascripts/web-dev-listener.js
index 46251c231..5a52e7384 100644
--- a/app/assets/javascripts/web-dev-listener.js
+++ b/app/assets/javascripts/web-dev-listener.js
@@ -19,13 +19,12 @@ 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);
+ allowed = allowed || pattern.test(origin);
});
if (!allowed) {
console.log('Ignoring message from bad origin:', origin);
return;
}
- //console.log(event);
var data = event.data;
var source = event.source;
switch (data.type) {
@@ -69,25 +68,25 @@ function update({ dom, styles, scripts }) {
domChanges.reduce(deku.dom.update(dispatch, context), concreteDom); // Rerender
var scriptChanges = virtualScripts.map(function(virtualScript, index){
- return deku.diff.diffNode(virtualScripts[index], scripts[index]);
+ return deku.diff.diffNode(virtualScripts[index], scripts[index]);
});
scriptChanges.forEach(function(scriptChange, index){
- scriptChange.reduce(deku.dom.update(dispatch, context), concreteStyles[index])
+ scriptChange.reduce(deku.dom.update(dispatch, context), concreteStyles[index])
});
var styleChanges = virtualStyles.map(function(virtualStyle, index){
- return deku.diff.diffNode(virtualStyles[index], styles[index]);
+ return deku.diff.diffNode(virtualStyles[index], styles[index]);
});
styleChanges.forEach(function(styleChange, index){
- styleChange.reduce(deku.dom.update(dispatch, context), concreteStyles[index])
+ styleChange.reduce(deku.dom.update(dispatch, context), concreteStyles[index])
});
virtualDom = dom;
}
function checkGoals(goals, source, origin) {
- // Check right now and also in one second, since our 1-second CSS transition might be affecting things until it is done.
- doCheckGoals(goals, source, origin);
- _.delay(function() { doCheckGoals(goals, source, origin); }, 1001);
+ // Check right now and also in one second, since our 1-second CSS transition might be affecting things until it is done.
+ doCheckGoals(goals, source, origin);
+ _.delay(function() { doCheckGoals(goals, source, origin); }, 1001);
}
function doCheckGoals(goals, source, origin) {
@@ -107,7 +106,7 @@ function doCheckGoals(goals, source, origin) {
if (!_.isEqual(newGoalStates, goalStates)) {
goalStates = newGoalStates;
var overallStatus = overallSuccess ? 'success' : null; // Can't really get to 'failure', just 'incomplete', which is represented by null here
- source.postMessage({type: 'goals-updated', goalStates: goalStates, overallStatus: overallStatus}, origin);
+ source.postMessage({type: 'goals-updated', goalStates: goalStates, overallStatus: overallStatus}, origin);
}
}
diff --git a/app/views/play/level/WebSurfaceView.coffee b/app/views/play/level/WebSurfaceView.coffee
index f4dded462..fb17410c5 100644
--- a/app/views/play/level/WebSurfaceView.coffee
+++ b/app/views/play/level/WebSurfaceView.coffee
@@ -10,7 +10,7 @@ module.exports = class WebSurfaceView extends CocoView
initialize: (options) ->
@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
+ # Consider https://www.npmjs.com/package/css-select to do this on virtualDom instead of in iframe on concreteDOM
super(options)
afterRender: ->
From ce137f00cb843eaed52581a168cf9d2a95a1aa30 Mon Sep 17 00:00:00 2001
From: phoenixeliot
Date: Wed, 20 Jul 2016 17:04:01 -0700
Subject: [PATCH 57/58] Fix level preview CSS
---
app/styles/play/level/loading.sass | 17 ++++++++++-------
1 file changed, 10 insertions(+), 7 deletions(-)
diff --git a/app/styles/play/level/loading.sass b/app/styles/play/level/loading.sass
index ab692465f..39f584321 100644
--- a/app/styles/play/level/loading.sass
+++ b/app/styles/play/level/loading.sass
@@ -74,13 +74,6 @@ $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
@@ -188,3 +181,13 @@ $UNVEIL_TIME: 1.2s
left: 48px
right: 77px
width: auto
+
+
+#level-view.web-dev
+ #loading-details.preview
+ @media screen and ( min-height: 900px )
+ background: transparent
+ border: 1px solid transparent
+ border-width: 124px 76px 64px 40px
+ border-image: url(/images/level/code_editor_background.png) 124 76 64 40 fill round
+ padding: 0 35px 0 15px
From 2ebf94c3db8c0fdf61a38244fddf8dc14804480a Mon Sep 17 00:00:00 2001
From: phoenixeliot
Date: Wed, 20 Jul 2016 17:20:21 -0700
Subject: [PATCH 58/58] Combine extracted script/style tags
---
app/assets/javascripts/web-dev-listener.js | 28 +++++-------
app/views/play/level/WebSurfaceView.coffee | 53 ++++++++++++++--------
2 files changed, 46 insertions(+), 35 deletions(-)
diff --git a/app/assets/javascripts/web-dev-listener.js b/app/assets/javascripts/web-dev-listener.js
index 5a52e7384..58f11db96 100644
--- a/app/assets/javascripts/web-dev-listener.js
+++ b/app/assets/javascripts/web-dev-listener.js
@@ -52,8 +52,8 @@ function create({ dom, styles, scripts }) {
virtualStyles = styles;
virtualScripts = scripts;
concreteDom = deku.dom.create(dom);
- concreteStyles = styles.map(function(style){return deku.dom.create(style)});
- concreteScripts = scripts.map(function(script){return deku.dom.create(script)});
+ concreteStyles = deku.dom.create(styles);
+ concreteScripts = deku.dom.create(scripts);
// TODO: target the actual HTML tag and combine our initial structure for styles/scripts/tags with theirs
// TODO: :after elements don't seem to work? (:before do)
$('body').first().empty().append(concreteDom);
@@ -64,23 +64,19 @@ function create({ dom, styles, scripts }) {
function update({ dom, styles, scripts }) {
function dispatch() {} // Might want to do something here in the future
var context = {}; // Might want to use this to send shared state to every component
+
var domChanges = deku.diff.diffNode(virtualDom, dom);
domChanges.reduce(deku.dom.update(dispatch, context), concreteDom); // Rerender
-
- var scriptChanges = virtualScripts.map(function(virtualScript, index){
- return deku.diff.diffNode(virtualScripts[index], scripts[index]);
- });
- scriptChanges.forEach(function(scriptChange, index){
- scriptChange.reduce(deku.dom.update(dispatch, context), concreteStyles[index])
- });
-
- var styleChanges = virtualStyles.map(function(virtualStyle, index){
- return deku.diff.diffNode(virtualStyles[index], styles[index]);
- });
- styleChanges.forEach(function(styleChange, index){
- styleChange.reduce(deku.dom.update(dispatch, context), concreteStyles[index])
- });
+
+ var scriptChanges = deku.diff.diffNode(virtualScripts, scripts);
+ scriptChanges.reduce(deku.dom.update(dispatch, context), concreteScripts); // Rerender
+
+ var styleChanges = deku.diff.diffNode(virtualStyles, styles);
+ styleChanges.reduce(deku.dom.update(dispatch, context), concreteStyles); // Rerender
+
virtualDom = dom;
+ virtualStyles = styles;
+ virtualScripts = scripts;
}
function checkGoals(goals, source, origin) {
diff --git a/app/views/play/level/WebSurfaceView.coffee b/app/views/play/level/WebSurfaceView.coffee
index fb17410c5..7ee8c95c9 100644
--- a/app/views/play/level/WebSurfaceView.coffee
+++ b/app/views/play/level/WebSurfaceView.coffee
@@ -46,25 +46,40 @@ module.exports = class WebSurfaceView extends CocoView
deku.element(elem.name, elem.attribs, (@dekuify(c) for c in elem.children ? []))
extractStylesAndScripts: (dekuTree) ->
- #base case
- if dekuTree.type is '#text'
- return { virtualDom: dekuTree, styles: [], scripts: [] }
- if dekuTree.type is 'style'
- console.log 'Found a style: ', dekuTree
- return { styles: [dekuTree], scripts: [] }
- if dekuTree.type is 'script'
- console.log 'Found a script: ', dekuTree
- return { styles: [], scripts: [dekuTree] }
- # recurse over children
- childStyles = []
- childScripts = []
- dekuTree.children?.forEach (dekuChild, index) =>
- { virtualDom, styles, scripts } = @extractStylesAndScripts(dekuChild)
- dekuTree.children[index] = virtualDom
- childStyles = childStyles.concat(styles)
- childScripts = childScripts.concat(scripts)
- dekuTree.children = _.filter dekuTree.children # Remove the nodes we extracted
- return { virtualDom: dekuTree, scripts: childScripts, styles: childStyles }
+ recurse = (dekuTree) ->
+ #base case
+ if dekuTree.type is '#text'
+ return { virtualDom: dekuTree, styles: [], scripts: [] }
+ if dekuTree.type is 'style'
+ console.log 'Found a style: ', dekuTree
+ return { styles: [dekuTree], scripts: [] }
+ if dekuTree.type is 'script'
+ console.log 'Found a script: ', dekuTree
+ return { styles: [], scripts: [dekuTree] }
+ # recurse over children
+ childStyles = []
+ childScripts = []
+ dekuTree.children?.forEach (dekuChild, index) =>
+ { virtualDom, styles, scripts } = recurse(dekuChild)
+ dekuTree.children[index] = virtualDom
+ childStyles = childStyles.concat(styles)
+ childScripts = childScripts.concat(scripts)
+ dekuTree.children = _.filter dekuTree.children # Remove the nodes we extracted
+ return { virtualDom: dekuTree, scripts: childScripts, styles: childStyles }
+
+ { virtualDom, scripts, styles } = recurse(dekuTree)
+ combinedScripts = @combineNodes('script', scripts)
+ combinedStyles = @combineNodes('style', styles)
+ return { virtualDom, scripts: combinedScripts, styles: combinedStyles }
+
+ combineNodes: (type, nodes) ->
+ if _.any(nodes, (node) -> node.type isnt type)
+ throw new Error("Can't combine nodes of different types. (Got #{nodes.map (n) -> n.type})")
+ children = nodes.map((n) -> n.children).reduce(((a,b) -> a.concat(b)), [])
+ if _.isEmpty(children)
+ deku.element(type, {})
+ else
+ deku.element(type, {}, children)
onIframeMessage: (event) =>
origin = event.origin or event.originalEvent.origin