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: -> '