From 25e348c5ad749693e853eaf12e2c1169fb1d115a Mon Sep 17 00:00:00 2001
From: Scott Erickson
Date: Tue, 12 Jul 2016 14:07:10 -0700
Subject: [PATCH 01/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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 ![python - image description](image url) for each non-target language.
@@ -290,12 +290,12 @@ module.exports.filterMarkdownCodeLanguages = (text, language) ->
return text
module.exports.aceEditModes = aceEditModes =
- 'javascript': 'ace/mode/javascript'
- 'coffeescript': 'ace/mode/coffee'
- 'python': 'ace/mode/python'
- 'java': 'ace/mode/java'
- 'lua': 'ace/mode/lua'
- 'java': 'ace/mode/java'
+ javascript: 'ace/mode/javascript'
+ coffeescript: 'ace/mode/coffee'
+ python: 'ace/mode/python'
+ lua: 'ace/mode/lua'
+ java: 'ace/mode/java'
+ html: 'ace/mode/html'
module.exports.initializeACE = (el, codeLanguage) ->
contents = $(el).text().trim()
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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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 ![description](url)
@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/62] 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/62] 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/62] 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/62] 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/62] 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 ![description](url)
@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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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/62] 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 15bcae8a17c3fe464858b4e392870d89cc10bd37 Mon Sep 17 00:00:00 2001
From: Matt Lott
Date: Wed, 20 Jul 2016 17:06:51 -0700
Subject: [PATCH 58/62] Show API popup entirely over surface
Covering user code causes problems for players.
---
app/styles/play/level/tome/spell_palette_entry.sass | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/styles/play/level/tome/spell_palette_entry.sass b/app/styles/play/level/tome/spell_palette_entry.sass
index 9c0604229..7a81978aa 100644
--- a/app/styles/play/level/tome/spell_palette_entry.sass
+++ b/app/styles/play/level/tome/spell_palette_entry.sass
@@ -54,6 +54,7 @@ body:not(.dialogue-view-active)
.spell-palette-popover.popover
// Only those popovers which are our direct children (spell documentation)
+ left: auto !important
max-width: 600px
padding: 0
border-style: solid
@@ -81,7 +82,6 @@ body:not(.dialogue-view-active)
@include animation(jiggle .3s infinite)
&.pinned
- left: auto !important
top: 50px !important
right: 45%
// bottom: 151px
From 2ebf94c3db8c0fdf61a38244fddf8dc14804480a Mon Sep 17 00:00:00 2001
From: phoenixeliot
Date: Wed, 20 Jul 2016 17:20:21 -0700
Subject: [PATCH 59/62] 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
From 11ba5333d4f43d32349d8df795185f39f4e9e409 Mon Sep 17 00:00:00 2001
From: UltCombo
Date: Wed, 20 Jul 2016 23:56:08 -0300
Subject: [PATCH 60/62] Campaign view: fix level titles getting cut off
E.g.:
![Forest - level titles cut off](http://i.imgur.com/cfIbyXv.png)
---
app/styles/play/campaign-view.sass | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/app/styles/play/campaign-view.sass b/app/styles/play/campaign-view.sass
index 91900ba65..ddfea3db6 100644
--- a/app/styles/play/campaign-view.sass
+++ b/app/styles/play/campaign-view.sass
@@ -182,7 +182,7 @@ $gameControlMargin: 30px
border-radius: $levelClickRadius
.tooltip
- z-index: 2
+ z-index: 3
pointer-events: none
.tooltip-arrow
@@ -402,7 +402,7 @@ $gameControlMargin: 30px
.player-name
margin-left: 45px
-
+
a
color: white
@@ -624,4 +624,3 @@ body.ipad #campaign-view
body[lang='ru'] .portals h2
font-size: 26px
-
From 4226cd098d3cd6f5d3bb73b99934f79c0adf5a35 Mon Sep 17 00:00:00 2001
From: Matt Lott
Date: Wed, 20 Jul 2016 21:03:48 -0700
Subject: [PATCH 61/62] Shrink campaign map flags, hovers, click areas
---
app/styles/play/campaign-view.sass | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/app/styles/play/campaign-view.sass b/app/styles/play/campaign-view.sass
index 91900ba65..1e48f3343 100644
--- a/app/styles/play/campaign-view.sass
+++ b/app/styles/play/campaign-view.sass
@@ -6,10 +6,10 @@ $mapWidth: 2350
$levelDotWidth: 2%
$levelDotHeight: $levelDotWidth * $mapWidth / $mapHeight
$levelDotZ: $levelDotHeight * 0.25
-$levelDotHoverZ: $levelDotZ * 2
+$levelDotHoverZ: $levelDotZ * 1.5
$levelDotShadowWidth: 0.8 * $levelDotWidth
$levelDotShadowHeight: 0.8 * $levelDotHeight
-$levelClickRadius: 40px
+$levelClickRadius: 20px
$gameControlSize: 80px
$gameControlMargin: 30px
@@ -105,7 +105,7 @@ $gameControlMargin: 30px
.level-difficulty-banner-text
position: absolute
- bottom: 170%
+ bottom: 80%
pointer-events: none
color: rgb(246, 208, 2)
text-shadow: 0px 1px 0px black
@@ -117,8 +117,7 @@ $gameControlMargin: 30px
img.banner
position: absolute
bottom: 38%
- left: -50%
- width: 200%
+ width: 100%
pointer-events: none
img.star
From 436896de4a203f60ca780f736bf133b4ab2d5f54 Mon Sep 17 00:00:00 2001
From: Nick Winter
Date: Thu, 21 Jul 2016 09:06:57 -0700
Subject: [PATCH 62/62] Update /about jobs
---
app/locale/en.coffee | 3 ++-
app/templates/about.jade | 20 +++-----------------
2 files changed, 5 insertions(+), 18 deletions(-)
diff --git a/app/locale/en.coffee b/app/locale/en.coffee
index edd918388..6e14070c0 100644
--- a/app/locale/en.coffee
+++ b/app/locale/en.coffee
@@ -781,7 +781,7 @@
mission_description_1: "Programming is magic. It's the ability to create things from pure imagination. We started CodeCombat to give learners the feeling of wizardly power at their fingertips by using typed code."
mission_description_2: "As it turns out, that enables them to learn faster too. WAY faster. It's like having a conversation instead of reading a manual. We want to bring that conversation to every school and to every student, because everyone should have the chance to learn the magic of programming."
team_title: "Meet the CodeCombat team"
- team_values: "We value open and respectful dialog, where the best idea wins. Our decisions are grounded in customer research and our process is focused on delivering tangible results for them. Everyone is hands-on, from our CEO to our Github contributors, because we value growth and learning in our team."
+ team_values: "We value open and respectful dialog, where the best idea wins. Our decisions are grounded in customer research and our process is focused on delivering tangible results for them. Everyone is hands-on, from our CEO to our GitHub contributors, because we value growth and learning in our team."
nick_title: "Cofounder, CEO"
nick_blurb: "Motivation Guru"
matt_title: "Cofounder, CTO"
@@ -802,6 +802,7 @@
phoenix_title: "Software Engineer"
nolan_title: "Territory Manager"
elliot_title: "Partnership Manager"
+ lisa_title: "Market Development Rep"
retrostyle_title: "Illustration"
retrostyle_blurb: "RetroStyle Games"
jose_title: "Music"
diff --git a/app/templates/about.jade b/app/templates/about.jade
index b66801346..b7f3d81ac 100644
--- a/app/templates/about.jade
+++ b/app/templates/about.jade
@@ -143,7 +143,7 @@ block content
img(src="/images/pages/about/lisa_small.png").img-thumbnail
.team-bio
h6.label.team-name Lisa Wu
- small Marketing Development Rep
+ small(data-i18n="about.lisa_title")
br
// Part time / contract
@@ -274,22 +274,8 @@ block content
li.small(data-i18n="about.jobs_benefit_7")
.col-sm-6.col-md-5.col-md-offset-1.col-lg-4.col-lg-offset-0
.job-listing
- h5 Marketing Manager
- .text-center
- small.label
- | San Francisco • Fulltime
- p.small We're looking for an amazing marketer to help us fill our funnel and keep the pipeline growing. As our Marketing Manager, you'll be responsible for driving traffic to our website through content creation and converting those visitors into leads and customers using both automated and personalized content.
- a.job-link.btn.btn-lg.btn-navy(href="https://jobs.lever.co/codecombat/1033ec13-d4a0-498d-99e0-628afdb56fb5" rel="external")
- span(data-i18n="about.learn_more")
- .col-sm-6.col-md-5.col-md-offset-1.col-lg-4.col-lg-offset-0
- .job-listing
- h5 Sales Representative
- .text-center
- small.label
- | San Francisco • Fulltime
- p.small School districts are scrambling to offer computer science classes to all their students as a core subject. They have had no solution, because they can't afford to hire enough programming teachers – until now.
- a.job-link.btn.btn-lg.btn-navy(href="https://jobs.lever.co/codecombat/3f6ff123-16ce-4ecb-aba3-dcf4e8927c47" rel="external")
- | Learn More
+ h5 (No Open Roles)
+ p.small Check back later for updates on new positions at CodeCombat.
.col-sm-6.col-md-5.col-lg-4
.job-listing
h5(data-i18n="about.jobs_custom_title")