diff --git a/app/application.coffee b/app/application.coffee
index 0e0c99104..ecd4e9b53 100644
--- a/app/application.coffee
+++ b/app/application.coffee
@@ -37,6 +37,8 @@ preload = (arrayOfImages) ->
Application = initialize: ->
Router = require('Router')
@isProduction = -> document.location.href.search('codecombat.com') isnt -1
+ @isIPadApp = webkit?.messageHandlers? and navigator.userAgent?.indexOf('iPad') isnt -1
+ $('body').addClass 'ipad' if @isIPadApp
@tracker = new Tracker()
@facebookHandler = new FacebookHandler()
@gplusHandler = new GPlusHandler()
diff --git a/app/initialize.coffee b/app/initialize.coffee
index 945a3ab27..db0078ea1 100644
--- a/app/initialize.coffee
+++ b/app/initialize.coffee
@@ -21,20 +21,16 @@ definitionSchemas =
init = ->
watchForErrors()
+ setUpIOSLogging()
path = document.location.pathname
testing = path.startsWith '/test'
demoing = path.startsWith '/demo'
initializeServices() unless testing or demoing
-
- # Set up Backbone.Mediator schemas
- setUpDefinitions()
- setUpChannels()
- Backbone.Mediator.setValidationEnabled document.location.href.search(/codecombat.com/) is -1
+ setUpBackboneMediator()
app.initialize()
Backbone.history.start({ pushState: true })
handleNormalUrls()
setUpMoment() # Set up i18n for moment
-
treemaExt = require 'treema-ext'
treemaExt.setup()
@@ -59,13 +55,15 @@ handleNormalUrls = ->
return false
-setUpChannels = ->
- for channel of channelSchemas
- Backbone.Mediator.addChannelSchemas channelSchemas[channel]
-
-setUpDefinitions = ->
- for definition of definitionSchemas
- Backbone.Mediator.addDefSchemas definitionSchemas[definition]
+setUpBackboneMediator = ->
+ Backbone.Mediator.addDefSchemas schemas for definition, schemas of definitionSchemas
+ Backbone.Mediator.addChannelSchemas schemas for channel, schemas of channelSchemas
+ Backbone.Mediator.setValidationEnabled document.location.href.search(/codecombat.com/) is -1
+ if webkit?.messageHandlers
+ originalPublish = Backbone.Mediator.publish
+ Backbone.Mediator.publish = ->
+ originalPublish.apply Backbone.Mediator, arguments
+ webkit.messageHandlers.backboneEventHandler?.postMessage channel: arguments[0], event: serializeForIOS(arguments[1] ? {})
setUpMoment = ->
{me} = require 'lib/auth'
@@ -94,11 +92,53 @@ watchForErrors = ->
return if currentErrors >= 3
return unless me.isAdmin() or document.location.href.search(/codecombat.com/) is -1 or document.location.href.search(/\/editor\//) isnt -1
++currentErrors
- msg = "Error: #{msg}
Check the JS console for more."
+ message = "Error: #{msg}
Check the JS console for more."
#msg += "\nLine: #{line}" if line?
#msg += "\nColumn: #{col}" if col?
#msg += "\nError: #{error}" if error?
#msg += "\nStack: #{stack}" if stack = error?.stack
- noty text: msg, layout: 'topCenter', type: 'error', killer: false, timeout: 5000, dismissQueue: true, maxVisible: 3, callback: {onClose: -> --currentErrors}
+ noty text: message, layout: 'topCenter', type: 'error', killer: false, timeout: 5000, dismissQueue: true, maxVisible: 3, callback: {onClose: -> --currentErrors}
+ Backbone.Mediator.publish 'application:error', message: msg # For iOS app
+
+setUpIOSLogging = ->
+ return unless webkit?.messageHandlers
+ for level in ['debug', 'log', 'info', 'warn', 'error']
+ do (level) ->
+ originalLog = console[level]
+ console[level] = ->
+ originalLog.apply console, arguments
+ try
+ webkit?.messageHandlers?.consoleLogHandler?.postMessage level: level, arguments: (a?.toString?() ? ('' + a) for a in arguments)
+ catch e
+ webkit?.messageHandlers?.consoleLogHandler?.postMessage level: level, arguments: ['could not post log: ' + e]
+
+# This is so hacky... hopefully it's restrictive enough to not be slow.
+# We could also keep a list of events we are actually subscribed for and only try to send those over.
+seen = null
+window.serializeForIOS = serializeForIOS = (obj, depth=3) ->
+ return {} unless depth
+ root = not seen?
+ seen ?= []
+ clone = {}
+ keysHandled = 0
+ for own key, value of obj
+ continue if ++keysHandled > 20
+ if not value
+ clone[key] = value
+ else if value is window or value.firstElementChild or value.preventDefault
+ null # Don't include these things
+ else if value in seen
+ null # No circular references
+ else if _.isArray value
+ clone[key] = (serializeForIOS(child, depth - 1) for child in value)
+ seen.push value
+ else if _.isObject value
+ value = value.attributes if value.id and value.attributes
+ clone[key] = serializeForIOS value, depth - 1
+ seen.push value
+ else
+ clone[key] = value
+ seen = null if root
+ clone
$ -> init()
diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee
index 78be93925..b809742c6 100644
--- a/app/lib/surface/Surface.coffee
+++ b/app/lib/surface/Surface.coffee
@@ -77,8 +77,8 @@ module.exports = Surface = class Surface extends CocoClass
'ctrl+\\, ⌘+\\': 'onToggleDebug'
'ctrl+o, ⌘+o': 'onTogglePathFinding'
-
-
+
+
#- Initialization
constructor: (@world, @canvas, givenOptions) ->
@@ -136,7 +136,7 @@ module.exports = Surface = class Surface extends CocoClass
#- Setting the world
-
+
setWorld: (@world) ->
@worldLoaded = true
lastFrame = Math.min(@getCurrentFrame(), @world.frames.length - 1)
@@ -227,10 +227,10 @@ module.exports = Surface = class Surface extends CocoClass
++@totalFramesDrawn
@stage.update e
-
-
+
+
#- Setting play/pause and progress
-
+
setProgress: (progress, scrubDuration=500) ->
progress = Math.max(Math.min(progress, 1), 0.0)
@@ -309,7 +309,7 @@ module.exports = Surface = class Surface extends CocoClass
#- Changes and events that only need to happen when the frame has changed
-
+
onFrameChanged: (force) ->
@currentFrame = Math.min(@currentFrame, @world.frames.length)
@debugDisplay?.updateFrame @currentFrame
@@ -335,8 +335,8 @@ module.exports = Surface = class Surface extends CocoClass
getProgress: -> @currentFrame / @world.frames.length
-
-
+
+
#- Subscription callbacks
onToggleDebug: (e) ->
@@ -484,7 +484,7 @@ module.exports = Surface = class Surface extends CocoClass
Backbone.Mediator.publish 'surface:mouse-scrolled', event unless @disabled
-
+
#- Canvas callbacks
onResize: (e) =>
@@ -509,7 +509,8 @@ module.exports = Surface = class Surface extends CocoClass
## newHeight = Math.min 589, newHeight
#@canvas.width newWidth
#@canvas.height newHeight
- @canvas.attr width: newWidth, height: newHeight
+ scaleFactor = if application.isIPadApp then 2 else 1 # Retina
+ @canvas.attr width: newWidth * scaleFactor, height: newHeight * scaleFactor
@stage.scaleX *= newWidth / oldWidth
@stage.scaleY *= newHeight / oldHeight
@camera.onResize newWidth, newHeight
@@ -517,7 +518,7 @@ module.exports = Surface = class Surface extends CocoClass
#- Real-time playback
-
+
onRealTimePlaybackWaiting: (e) ->
@onRealTimePlaybackStarted e
@@ -539,8 +540,8 @@ module.exports = Surface = class Surface extends CocoClass
@canvas.toggleClass 'flag-color-selected', Boolean(e.color)
e.pos = @camera.screenToWorld @mouseScreenPos if @mouseScreenPos
-
-
+
+
#- Paths - TODO: move to SpriteBoss? but only update on frame drawing instead of on every frame update?
updatePaths: ->
@@ -561,9 +562,9 @@ module.exports = Surface = class Surface extends CocoClass
return if not @paths
@paths.parent.removeChild @paths
@paths = null
-
-
-
+
+
+
#- Screenshot
screenshot: (scale=0.25, format='image/jpeg', quality=0.8, zoom=2) ->
@@ -578,10 +579,10 @@ module.exports = Surface = class Surface extends CocoClass
@stage.uncache()
imageData
-
-
+
+
#- Path finding debugging
-
+
onTogglePathFinding: (e) ->
e?.preventDefault?()
@hidePathFinding()
@@ -640,11 +641,11 @@ module.exports = Surface = class Surface extends CocoClass
.lineTo(v2.x, v2.y)
.endStroke()
container.addChild shape
-
-
-
+
+
+
#- Teardown
-
+
destroy: ->
@camera?.destroy()
createjs.Ticker.removeEventListener('tick', @tick)
@@ -672,4 +673,3 @@ module.exports = Surface = class Surface extends CocoClass
clearTimeout @surfacePauseTimeout if @surfacePauseTimeout
clearTimeout @surfaceZoomPauseTimeout if @surfaceZoomPauseTimeout
super()
-
diff --git a/app/models/SuperModel.coffee b/app/models/SuperModel.coffee
index 930612edb..afaceeda7 100644
--- a/app/models/SuperModel.coffee
+++ b/app/models/SuperModel.coffee
@@ -195,6 +195,7 @@ module.exports = class SuperModel extends Backbone.Model
@progress = newProg
@trigger('update-progress', @progress)
@trigger('loaded-all') if @finished()
+ Backbone.Mediator.publish 'supermodel:load-progress-changed', progress: @progress
setMaxProgress: (@maxProgress) ->
resetProgress: -> @progress = 0
diff --git a/app/schemas/subscriptions/misc.coffee b/app/schemas/subscriptions/misc.coffee
index fbaa4b813..209564359 100644
--- a/app/schemas/subscriptions/misc.coffee
+++ b/app/schemas/subscriptions/misc.coffee
@@ -4,6 +4,10 @@ module.exports =
'application:idle-changed': c.object {},
idle: {type: 'boolean'}
+ 'application:error': c.object {},
+ message: {type: 'string'}
+ stack: {type: 'string'}
+
'audio-player:loaded': c.object {required: ['sender']},
sender: {type: 'object'}
@@ -31,3 +35,6 @@ module.exports =
'ladder:game-submitted': c.object {required: ['session', 'level']},
session: {type: 'object'}
level: {type: 'object'}
+
+ 'supermodel:load-progress-changed': c.object {required: ['progress']},
+ progress: {type: 'number', minimum: 0, maximum: 1}
diff --git a/app/schemas/subscriptions/tome.coffee b/app/schemas/subscriptions/tome.coffee
index ced070475..fde7c41d1 100644
--- a/app/schemas/subscriptions/tome.coffee
+++ b/app/schemas/subscriptions/tome.coffee
@@ -109,3 +109,5 @@ module.exports =
'tome:toggle-maximize': c.object {title: 'Toggle Maximize', description: 'Published when we want to make the Tome take up most of the screen'}
'tome:maximize-toggled': c.object {title: 'Maximize Toggled', description: 'Published when the Tome has changed maximize/minimize state.'}
+
+ 'tome:select-primary-sprite': c.object {title: 'Select Primary Sprite', description: 'Published to get the most important sprite\'s code selected.'}
diff --git a/app/styles/play/level.sass b/app/styles/play/level.sass
index cf4edfba7..c3857b156 100644
--- a/app/styles/play/level.sass
+++ b/app/styles/play/level.sass
@@ -210,3 +210,56 @@ html.fullscreen-editor
width: 95%
height: 100%
right: 0
+
+body.ipad #level-view
+ // Full-width Surface, preserving aspect ratio.
+ height: 1024px * (589 / 924)
+ overflow: hidden
+
+ #code-area, .footer, #thang-hud
+ display: none
+
+ #control-bar-view
+ position: absolute
+ background: transparent
+ z-index: 1
+
+ .home
+ i
+ @include opacity(1)
+ .home-text
+ display: none
+
+ .title
+ left: 20%
+ width: 60%
+ text-align: center
+ color: white
+
+ a, .editor-dash
+ display: none
+
+ #level-chat-view
+ bottom: 40px
+
+ #playback-view
+ background-color: transparent
+ z-index: 3
+ bottom: 30px
+ margin-bottom: -30px
+
+ button
+ background-color: #333
+
+ .scrubber .progress
+ background-color: rgba(255, 255, 255, 0.4)
+
+ #canvas-wrapper, #control-bar-view, #playback-view, #thang-hud
+ width: 100%
+
+ #canvas-wrapper
+ height: 653px
+
+ canvas#surface
+ margin: 0 auto
+ overflow: hidden
diff --git a/app/templates/play/level/control_bar.jade b/app/templates/play/level/control_bar.jade
index 087581d61..006072ea5 100644
--- a/app/templates/play/level/control_bar.jade
+++ b/app/templates/play/level/control_bar.jade
@@ -2,11 +2,11 @@ h4.home
a(href=homeLink || "/")
i.icon-home.icon-white
- span(data-i18n="play_level.home") Home
+ span(data-i18n="play_level.home").home-text Home
h4.title
| #{worldName}
- span.spl.spr -
+ span.spl.spr.editor-dash -
a(href=editorLink, data-i18n="nav.editor", title="Open " + worldName + " in the Level Editor") Editor
if multiplayerSession
- found = false
diff --git a/app/views/kinds/CocoView.coffee b/app/views/kinds/CocoView.coffee
index 41e974a01..19aab42d2 100644
--- a/app/views/kinds/CocoView.coffee
+++ b/app/views/kinds/CocoView.coffee
@@ -66,7 +66,7 @@ module.exports = class CocoView extends Backbone.View
$.noty.closeAll()
destroyAceEditor: (editor) ->
- # convenience method to make sure the ace editor is as destroyed as can be
+ # convenience method to make sure the ace editor is as destroyed as can be
return unless editor
session = editor.getSession()
session.setMode ''
@@ -155,7 +155,7 @@ module.exports = class CocoView extends Backbone.View
res.load()
@$el.find('.progress').show()
$(e.target).closest('.loading-error-alert').remove()
-
+
onSkipResource: (e) ->
res = @supermodel.getResource($(e.target).data('resource-index'))
return unless res and res.isFailed
@@ -319,6 +319,10 @@ module.exports = class CocoView extends Backbone.View
isMac: ->
navigator.platform.toUpperCase().indexOf('MAC') isnt -1
+ isIPadApp: ->
+ return @_isIPadApp if @_isIPadApp?
+ return @_isIPadApp = webkit?.messageHandlers? and navigator.userAgent?.indexOf('iPad') isnt -1
+
initSlider: ($el, startValue, changeCallback) ->
slider = $el.slider({animate: 'fast'})
slider.slider('value', startValue)
diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee
index 5c4b49889..fafe1db1d 100644
--- a/app/views/play/level/PlayLevelView.coffee
+++ b/app/views/play/level/PlayLevelView.coffee
@@ -131,6 +131,7 @@ module.exports = class PlayLevelView extends RootView
updateProgress: (progress) ->
super(progress)
return if @seenDocs
+ return if @isIPadApp()
return unless @levelLoader.session.loaded and @levelLoader.level.loaded
return unless showFrequency = @levelLoader.level.get('showsGuide')
session = @levelLoader.session
@@ -173,10 +174,11 @@ module.exports = class PlayLevelView extends RootView
@insertSubView @loadingView = new LevelLoadingView {}
@$el.find('#level-done-button').hide()
$('body').addClass('is-playing')
+ $('body').bind('touchmove', false) if @isIPadApp()
afterInsert: ->
super()
- @showWizardSettingsModal() if not me.get('name')
+ @showWizardSettingsModal() if not me.get('name') and not @isIPadApp()
# Partially Loaded Setup ####################################################
@@ -259,7 +261,7 @@ module.exports = class PlayLevelView extends RootView
@insertSubView new ChatView levelID: @levelID, sessionID: @session.id, session: @session
worldName = utils.i18n @level.attributes, 'name'
@controlBar = @insertSubView new ControlBarView {worldName: worldName, session: @session, level: @level, supermodel: @supermodel, playableTeams: @world.playableTeams}
- #Backbone.Mediator.publish('level:set-debug', debug: true) if me.displayName() is 'Nick'
+ Backbone.Mediator.publish('level:set-debug', debug: true) if @isIPadApp() # if me.displayName() is 'Nick'
initVolume: ->
volume = me.get('volume')
@@ -343,6 +345,8 @@ module.exports = class PlayLevelView extends RootView
if state.selected
# TODO: Should also restore selected spell here by saving spellName
Backbone.Mediator.publish 'level:select-sprite', thangID: state.selected, spellName: null
+ else if @isIPadApp()
+ Backbone.Mediator.publish 'tome:select-primary-sprite', {}
if state.playing?
Backbone.Mediator.publish 'level:set-playing', playing: state.playing
diff --git a/app/views/play/level/tome/ThangListEntryView.coffee b/app/views/play/level/tome/ThangListEntryView.coffee
index 52bfe8ce0..75d03ac22 100644
--- a/app/views/play/level/tome/ThangListEntryView.coffee
+++ b/app/views/play/level/tome/ThangListEntryView.coffee
@@ -80,11 +80,14 @@ module.exports = class ThangListEntryView extends CocoView
score += 9001 * _.size(s.thangs)
score
- onClick: (e) ->
- return unless @controlsEnabled
+ select: ->
@sortSpells()
Backbone.Mediator.publish 'level:select-sprite', thangID: @thang.id, spellName: @spells[0]?.name
+ onClick: (e) ->
+ return unless @controlsEnabled
+ @select()
+
onMouseEnter: (e) ->
return unless @controlsEnabled and @spells.length
@clearTimeouts()
diff --git a/app/views/play/level/tome/ThangListView.coffee b/app/views/play/level/tome/ThangListView.coffee
index 2b09f44e8..7f69d55d0 100644
--- a/app/views/play/level/tome/ThangListView.coffee
+++ b/app/views/play/level/tome/ThangListView.coffee
@@ -11,7 +11,8 @@ module.exports = class ThangListView extends CocoView
id: 'thang-list-view'
template: template
- subscriptions: {}
+ subscriptions:
+ 'tome:select-primary-sprite': 'onSelectPrimarySprite'
constructor: (options) ->
super options
@@ -89,6 +90,9 @@ module.exports = class ThangListView extends CocoView
@sortThangs()
@addThangListEntries()
+ onSelectPrimarySprite: (e) ->
+ @entries[0]?.select()
+
destroy: ->
entry.destroy() for entry in @entries
super()
diff --git a/test/demo/easeljs/WebGL.demo.coffee b/test/demo/easeljs/WebGL.demo.coffee
index 54b081cbe..4b2efe59a 100644
--- a/test/demo/easeljs/WebGL.demo.coffee
+++ b/test/demo/easeljs/WebGL.demo.coffee
@@ -4,7 +4,7 @@ librarianLib = require 'test/demo/fixtures/librarian'
class WebGLDemoView extends RootView
template: -> '