Game dev levels (#3810)

* Tweak API doc behavior and styling

* Instead of moving to the left during active dialogues, just move to the top
* Allow pointer events
* Adjust close button
* Re-enable pinning API docs for game-dev and web-dev levels

* Make sidebar in PlayGameDevLevelView stretch, better layout columns

* Set up content of PlayGameDevLevelView sidebar to scroll

* Add rest of PlayGameDevLevelView sidebar content, rework what loading looks like

* Finish PlayGameDevLevelView

* Add share area below
* Cover the brown background, paint it gray

* Tweak PlayGameDevLevelView

* Have progress bar show everything
* Fix Surface resize handling

* Fix PlayGameDevLevelView resizing incorrectly when playing

* Add GameDevVictoryModal to PlayGameDevLevelView

* Don't show missing-doctype annotation in Ace

* Hook up GameDevVictoryModal copy button

* Fix onChangeAnnotation runtime error

* Fix onLevelLoaded runtime error

* Have CourseVictoryModal link to /courses when course is done

* Trim, update CourseDetailsView

* Remove last vestiges of teacherMode
* Remove giant navigation buttons at top
* Quick switch to flat style

* Add analytics for game-dev

* Update Analytics events for gamedev

* Prefix event names with context
* Send to Mixpanel
* Include more properties

* Mostly set up indefinite play and autocast for game-dev levels

* Set up cast buttons and shortcut for game-dev

* Add rudimentary instructions when students play game-dev levels

* Couple tweaks

* fix a bit of code that expects frames to always stick around
* have PlayGameDevLevelView render a couple frames on load

* API Docs use 'game' instead of 'hero'

* Move tags to head without combining

* Add HTML comment-start string

Fixes missing entry point arrows

* Fix some whitespace
This commit is contained in:
Nick Winter 2016-07-28 13:39:58 -07:00 committed by GitHub
parent b7f916116d
commit d77625bc77
40 changed files with 533 additions and 255 deletions

View file

@ -3,7 +3,11 @@
window.addEventListener('message', receiveMessage, false);
var concreteDom;
var concreteStyles;
var concreteScripts;
var virtualDom;
var virtualStyles;
var virtualScripts;
var goalStates;
var allowedOrigins = [
@ -54,11 +58,31 @@ function create({ dom, styles, scripts }) {
concreteDom = deku.dom.create(dom);
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);
$('#player-styles').first().empty().append(concreteStyles);
$('#player-scripts').first().empty().append(concreteScripts);
replaceNodes('[for="player-styles"]', unwrapConcreteNodes(concreteStyles));
replaceNodes('[for="player-scripts"]', unwrapConcreteNodes(concreteScripts));
}
function unwrapConcreteNodes(wrappedNodes) {
return wrappedNodes.children;
}
function replaceNodes(selector, newNodes){
$newNodes = $(newNodes).clone()
$(selector + ':not(:first)').remove();
firstNode = $(selector).first();
$newNodes.attr('for', firstNode.attr('for'));
newFirstNode = $newNodes[0];
try {
firstNode.replaceWith(newFirstNode); // Removes newFirstNode from its array (!!)
} catch (e) {
console.log('Failed to update some nodes:', e);
}
$(newFirstNode).after($newNodes);
}
function update({ dom, styles, scripts }) {
@ -68,11 +92,13 @@ function update({ dom, styles, scripts }) {
var domChanges = deku.diff.diffNode(virtualDom, dom);
domChanges.reduce(deku.dom.update(dispatch, context), concreteDom); // Rerender
var scriptChanges = deku.diff.diffNode(virtualScripts, scripts);
scriptChanges.reduce(deku.dom.update(dispatch, context), concreteScripts); // Rerender
// var scriptChanges = deku.diff.diffNode(virtualScripts, scripts);
// scriptChanges.reduce(deku.dom.update(dispatch, context), concreteScripts); // Rerender
// replaceNodes('[for="player-scripts"]', unwrapConcreteNodes(concreteScripts));
var styleChanges = deku.diff.diffNode(virtualStyles, styles);
styleChanges.reduce(deku.dom.update(dispatch, context), concreteStyles); // Rerender
replaceNodes('[for="player-styles"]', unwrapConcreteNodes(concreteStyles));
virtualDom = dom;
virtualStyles = styles;

View file

@ -389,6 +389,8 @@ self.runWorld = function runWorld(args) {
self.world.preloading = args.preload;
self.world.headless = args.headless;
self.world.realTime = args.realTime;
self.world.indefiniteLength = args.indefiniteLength;
self.world.justBegin = args.justBegin;
self.goalManager = new GoalManager(self.world);
self.goalManager.setGoals(args.goals);
self.goalManager.setCode(args.userCodeMap);
@ -434,6 +436,9 @@ self.onWorldLoaded = function onWorldLoaded() {
var diff = t1 - self.t0;
var goalStates = self.goalManager.getGoalStates();
var totalFrames = self.world.totalFrames;
if(self.world.indefiniteLength) {
totalFrames = self.world.frames.length;
}
if(self.world.ended) {
var overallStatus = self.goalManager.checkOverallStatus();
var lastFrameHash = self.world.frames[totalFrames - 2].hash

View file

@ -35,9 +35,13 @@
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script>
<!-- Extracted player/level styles and scripts -->
<style id="player-styles">
<style for="level-styles">
</style>
<script id="player-scripts">
<script for="level-scripts">
</script>
<style for="player-styles">
</style>
<script for="player-scripts">
</script>
</head>
<body>

5
app/core/urls.coffee Normal file
View file

@ -0,0 +1,5 @@
module.exports =
playDevLevel: ({level, session, course}) ->
shareURL = "#{window.location.origin}/play/#{level.get('type')}-level/#{level.get('slug')}/#{session.id}"
shareURL += "?course=#{course.id}" if course
return shareURL

View file

@ -111,7 +111,9 @@ module.exports = class Angel extends CocoClass
when 'user-code-problem'
@publishGodEvent 'user-code-problem', problem: event.data.problem
when 'world-load-progress-changed'
@publishGodEvent 'world-load-progress-changed', progress: event.data.progress
progress = event.data.progress
progress = Math.min(progress, 0.9) if @work.indefiniteLength
@publishGodEvent 'world-load-progress-changed', { progress }
unless event.data.progress is 1 or @work.preload or @work.headless or @work.synchronous or @deserializationQueue.length or (@shared.firstWorld and not @shared.spectate)
@worker.postMessage func: 'serializeFramesSoFar' # Stream it!
@ -138,6 +140,7 @@ module.exports = class Angel extends CocoClass
return if @aborting
# Toggle BOX2D_ENABLED during deserialization so that if we have box2d in the namespace, the Collides Components still don't try to create bodies for deserialized Thangs upon attachment.
window.BOX2D_ENABLED = false
streamingWorld?.indefiniteLength = @work.indefiniteLength
@streamingWorld = World.deserialize serialized, @shared.worldClassMap, @shared.lastSerializedWorldFrames, @finishBeholdingWorld(goalStates), startFrame, endFrame, @work.level, streamingWorld
window.BOX2D_ENABLED = true
@shared.lastSerializedWorldFrames = serialized.frames
@ -145,7 +148,10 @@ module.exports = class Angel extends CocoClass
finishBeholdingWorld: (goalStates) -> (world) =>
return if @aborting or @destroyed
finished = world.frames.length is world.totalFrames
firstChangedFrame = world.findFirstChangedFrame @shared.world
if @work?.indefiniteLength and world.victory?
finished = true
world.totalFrames = world.frames.length
firstChangedFrame = if @work.indefiniteLength then 0 else world.findFirstChangedFrame @shared.world
eventType = if finished then 'new-world-created' else 'streaming-world-updated'
if finished
@shared.world = world

View file

@ -20,6 +20,7 @@ module.exports = class God extends CocoClass
options ?= {}
@retrieveValueFromFrame = _.throttle @retrieveValueFromFrame, 1000
@gameUIState ?= options.gameUIState or new GameUIState()
@indefiniteLength = options.indefiniteLength or false
super()
# Angels are all given access to this.
@ -71,9 +72,9 @@ module.exports = class God extends CocoClass
@lastFixedSeed = e.fixedSeed
@lastFlagHistory = (flag for flag in e.flagHistory when flag.source isnt 'code')
@lastDifficulty = e.difficulty
@createWorld e.spells, e.preload, e.realTime
@createWorld e.spells, e.preload, e.realTime, e.justBegin
createWorld: (spells, preload, realTime) ->
createWorld: (spells, preload, realTime, justBegin) ->
console.log "#{@nick}: Let there be light upon #{@level.name}! (preload: #{preload})"
userCodeMap = @getUserCodeMap spells
@ -107,6 +108,8 @@ module.exports = class God extends CocoClass
preload
synchronous: not Worker? # Profiling world simulation is easier on main thread, or we are IE9.
realTime
justBegin
indefiniteLength: @indefiniteLength and realTime
}
@angelsShare.workQueue.push work
angel.workIfIdle() for angel in @angelsShare.angels

View file

@ -92,9 +92,9 @@ module.exports = Surface = class Surface extends CocoClass
})
@realTimeInputEvents = @gameUIState.get('realTimeInputEvents')
@listenTo(@gameUIState, 'sprite:mouse-down', @onSpriteMouseDown)
@onResize = _.debounce @onResize, resizeDelay
@initEasel()
@initAudio()
@onResize = _.debounce @onResize, resizeDelay
$(window).on 'resize', @onResize
if @world.ended
_.defer => @setWorld @world
@ -131,8 +131,9 @@ module.exports = Surface = class Surface extends CocoClass
@handleEvents
})
@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.
unless @options.levelType is 'game-dev'
@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.
@initCoordinates()
@webGLStage.enableMouseOver(10)
@webGLStage.addEventListener 'stagemousemove', @onMouseMove
@ -227,6 +228,7 @@ module.exports = Surface = class Surface extends CocoClass
restoreWorldState: ->
frame = @world.getFrame(@getCurrentFrame())
return unless frame
frame.restoreState()
current = Math.max(0, Math.min(@currentFrame, @world.frames.length - 1))
if current - Math.floor(current) > 0.01 and Math.ceil(current) < @world.frames.length - 1
@ -325,12 +327,12 @@ module.exports = Surface = class Surface extends CocoClass
@surfacePauseTimeout = _.delay performToggle, 2000
@lankBoss.stop()
@trailmaster?.stop()
@playbackOverScreen.show()
@playbackOverScreen?.show()
else
performToggle()
@lankBoss.play()
@trailmaster?.play()
@playbackOverScreen.hide()
@playbackOverScreen?.hide()
@ -348,7 +350,7 @@ module.exports = Surface = class Surface extends CocoClass
world: @world
)
if @lastFrame < @world.frames.length and @currentFrame >= @world.totalFrames - 1
if (not @world.indefiniteLength) and @lastFrame < @world.frames.length and @currentFrame >= @world.totalFrames - 1
@ended = true
@setPaused true
Backbone.Mediator.publish 'surface:playback-ended', {}
@ -551,6 +553,9 @@ module.exports = Surface = class Surface extends CocoClass
if application.isIPadApp
newWidth = 1024
newHeight = newWidth / aspectRatio
else if @options.resizeStrategy is 'wrapper-size'
newWidth = $('#canvas-wrapper').width()
newHeight = newWidth / aspectRatio
else if @realTime or @options.spectateGame
pageHeight = $('#page-container').height() - $('#control-bar-view').outerHeight() - $('#playback-view').outerHeight()
newWidth = Math.min pageWidth, pageHeight * aspectRatio
@ -576,6 +581,7 @@ module.exports = Surface = class Surface extends CocoClass
return if newWidth is oldWidth and newHeight is oldHeight and not @options.spectateGame
return if newWidth < 200 or newHeight < 200
@normalCanvas.add(@webGLCanvas).attr width: newWidth, height: newHeight
@trigger 'resize', { width: newWidth, height: newHeight }
# Cannot do this to the webGLStage because it does not use scaleX/Y.
# Instead the LayerAdapter scales webGL-enabled layers.

View file

@ -28,6 +28,7 @@ module.exports = class World
debugging: false # Whether we are just rerunning to debug a world we've already cast
headless: false # Whether we are just simulating for goal states instead of all serialized results
framesSerializedSoFar: 0
framesClearedSoFar: 0
apiProperties: ['age', 'dt']
realTimeBufferMax: REAL_TIME_BUFFER_MAX / 1000
constructor: (@userCodeMap, classMap) ->
@ -96,6 +97,7 @@ module.exports = class World
loadFrames: (loadedCallback, errorCallback, loadProgressCallback, preloadedCallback, skipDeferredLoading, loadUntilFrame) ->
return if @aborted
@totalFrames = 2 if @justBegin
console.log 'Warning: loadFrames called on empty World (no thangs).' unless @thangs.length
continueLaterFn = =>
@loadFrames(loadedCallback, errorCallback, loadProgressCallback, preloadedCallback, skipDeferredLoading, loadUntilFrame) unless @destroyed
@ -116,7 +118,13 @@ module.exports = class World
@lastRealTimeUpdate ?= 0
frameToLoadUntil = if loadUntilFrame then loadUntilFrame + 1 else @totalFrames # Might stop early if debugging.
i = @frames.length
while i < frameToLoadUntil and i < @totalFrames
while true
if @indefiniteLength
break if not @realTime # realtime has been stopped
break if @victory? # game won or lost # TODO: give a couple seconds of buffer after victory is set instead of ending instantly
else
break if i >= frameToLoadUntil
break if i >= @totalFrames
return unless @shouldContinueLoading t1, loadProgressCallback, skipDeferredLoading, continueLaterFn
@adjustFlowSettings loadUntilFrame if @debugging
try
@ -379,6 +387,11 @@ module.exports = class World
@freeMemoryBeforeFinalSerialization() if @ended
startFrame = @framesSerializedSoFar
endFrame = @frames.length
if @indefiniteLength
toClear = Math.max(@framesSerializedSoFar-10, 0)
for i in _.range(@framesClearedSoFar, toClear)
@frames[i] = null
@framesClearedSoFar = @framesSerializedSoFar
#console.log "... world serializing frames from", startFrame, "to", endFrame, "of", @totalFrames
[transferableObjects, nontransferableObjects] = [0, 0]
serializedFlagHistory = (_.omit(_.clone(flag), 'processed') for flag in @flagHistory)
@ -525,6 +538,14 @@ module.exports = class World
perf.framesCPUTime = 0
w.frames = [] unless streamingWorld
clearTimeout @deserializationTimeout if @deserializationTimeout
if w.indefiniteLength
clearTo = Math.max(w.frames.length - 100, 0)
if clearTo > w.framesClearedSoFar
for i in _.range(w.framesClearedSoFar, clearTo)
w.frames[i] = null
w.framesClearedSoFar = clearTo
@deserializationTimeout = _.delay @deserializeSomeFrames, 1, o, w, finishedWorldCallback, perf, startFrame, endFrame
w # Return in-progress deserializing world
@ -588,6 +609,7 @@ module.exports = class World
lastPos = x: null, y: null
for frameIndex in [lastFrameIndex .. 0] by -1
frame = @frames[frameIndex]
continue unless frame # may have been evicted for game dev levels
if pos = frame.thangStateMap[thangID]?.getStateForProp 'pos'
pos = camera.worldToSurface {x: pos.x, y: pos.y} if camera # without z
if not lastPos.x? or (Math.abs(lastPos.x - pos.x) + Math.abs(lastPos.y - pos.y)) > 1

View file

@ -10,7 +10,7 @@ module.exports = class WorldFrame
getNextFrame: ->
# Optimized. Must be called while thangs are current at this frame.
nextTime = @time + @world.dt
return null if nextTime > @world.lifespan
return null if nextTime > @world.lifespan and not @world.indefiniteLength
@hash = @world.rand.seed
@hash += system.update() for system in @world.systems
nextFrame = new WorldFrame(@world, nextTime)

View file

@ -6,6 +6,7 @@ module.exports =
thang: {type: 'object'}
preload: {type: 'boolean'}
realTime: {type: 'boolean'}
justBegin: {type: 'boolean'}
'tome:cast-spells': c.object {title: 'Cast Spells', description: 'Published when spells are cast', required: ['spells', 'preload', 'realTime', 'submissionCount', 'flagHistory', 'difficulty', 'god']},
spells: {type: 'object'}
@ -16,6 +17,7 @@ module.exports =
flagHistory: {type: 'array'}
difficulty: {type: 'integer'}
god: {type: 'object'}
justBegin: {type: 'boolean'}
'tome:manual-cast': c.object {title: 'Manually Cast Spells', description: 'Published when you wish to manually recast all spells', required: []},
realTime: {type: 'boolean'}

View file

@ -0,0 +1,8 @@
#game-dev-victory-modal
.share-row
margin: 20px 0
#copy-url-input
width: 50%
margin: 0 10px
display: inline-block

View file

@ -1,9 +1,19 @@
#play-game-dev-level-view
.container-fluid
overflow: hidden
background: #333
padding: 15px
height: 100vh
#game-row
display: flex
#canvas-wrapper
width: 100%
position: relative
overflow: hidden
z-index: 0
border-radius: 5px
#webgl-surface
background-color: #333
@ -18,5 +28,34 @@
display: block
z-index: 2
#play-btn
text-transform: uppercase
#info-col
.panel
height: 100%
display: flex
flex-direction: column
.panel-body
flex-grow: 1
overflow: scroll
.panel-footer
min-height: 70px
#play-btn
text-transform: uppercase
#share-panel-body
display: flex
align-items: center
#share-text-div, #copy-url-div
flex-grow: 1
#share-text-div
margin-right: 20px
#copy-url-input
width: 50%
#copy-url-div
margin-left: 20px

View file

@ -158,6 +158,10 @@
.ace_gutter-cell.ace_error
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTM5jWRgMAAAAVdEVYdENyZWF0aW9uIFRpbWUAMi8xNy8wOCCcqlgAAAQRdEVYdFhNTDpjb20uYWRvYmUueG1wADw/eHBhY2tldCBiZWdpbj0iICAgIiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+Cjx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDQuMS1jMDM0IDQ2LjI3Mjk3NiwgU2F0IEphbiAyNyAyMDA3IDIyOjExOjQxICAgICAgICAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4YXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgogICAgICAgICA8eGFwOkNyZWF0b3JUb29sPkFkb2JlIEZpcmV3b3JrcyBDUzM8L3hhcDpDcmVhdG9yVG9vbD4KICAgICAgICAgPHhhcDpDcmVhdGVEYXRlPjIwMDgtMDItMTdUMDI6MzY6NDVaPC94YXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhhcDpNb2RpZnlEYXRlPjIwMDgtMDMtMjRUMTk6MDA6NDJaPC94YXA6TW9kaWZ5RGF0ZT4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAgICAgICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyI+CiAgICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2UvcG5nPC9kYzpmb3JtYXQ+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIDUdUmQAAAD5SURBVDiNpZMxagMxEEWfgiCXcB3IbXwD7zbaM0nNyjdIl1O4Dk7pbsslEFbEZFKsJsiJrGDy4YM0M//zRyAoINAJyB8cS43RwwIdMFrvaeE8DADxXqQ3Jstn6GaQ5L3M0GQxsyaZoJtA3r2XCS6o+FkvZkdOIG/eywl+UVHrqcYm4BNIjb1rPdXYBTivj3gVtZ5q/p8gAfPhcLOBamzKcW41UI1dgA/qez4bU6muUE0zwVYEgKeKkWruEnTHENg4R8pFZblCyY1zHEMgQTQAe9gB8cE5XkO4GhugmIk76L+z+Wzy6FzT4CWLXf5MF8upSdMB4gC9Xr4AiezTJHGxdq0AAAAASUVORK5CYII=)
// NOTE! This hides all info annotations because removing specific annotations with listeners means they flicker. This hides that flicker. see: SpellView.onChangeAnnotation
.ace_gutter-cell.ace_info
background-image: none
.ace_gutter-cell.entry-point:not(.next-entry-point):after
opacity: 0.5

View file

@ -46,23 +46,22 @@
color: rgb(243, 169, 49)
body:not(.dialogue-view-active)
body.dialogue-view-active
.spell-palette-popover.popover
right: 45%
min-width: 500px
margin-top: -17%
top: 50px !important
.spell-palette-popover.popover
// Only those popovers which are our direct children (spell documentation)
left: auto !important
right: 45%
max-width: 600px
min-width: 500px
padding: 0
border-style: solid
border-image: url(/images/level/popover_border_background.png) 16 12 fill round
border-width: 16px 12px
@include box-shadow(0 0 0 #000)
// Prevent flickering in weird scenarios where popover goes over its own property
pointer-events: none
// Jiggle animation
// TODO: consolidate with problem_alert.sass jiggle
@ -96,8 +95,8 @@ body:not(.dialogue-view-active)
.close
position: absolute
top: 5%
right: 5%
top: -7px
right: 2px
font-size: 28px
font-weight: bold
@include opacity(0.6)

View file

@ -91,6 +91,9 @@ body[lang='ru'], body[lang='uk'], body[lang='bg'], body[lang^='mk'], body[lang='
font-size: 40px
opacity: 0.5
hr
border-top: 1px solid gray
// Navbar
#main-nav.navbar

View file

@ -1,135 +1,87 @@
extends /templates/base
extends /templates/base-flat
block content
if me.isTeacher()
.alert.alert-danger.text-center
// DNT: Temporary
h3 ATTENTION TEACHERS:
p We are transitioning to a new classroom management system; this page will soon be student-only.
a(href="/teachers/classes") Go to teachers area.
.container.m-t-3
p
a(href="/courses", data-i18n="courses.back_courses")
if view.teacherMode
a(href="/teachers/classes", data-i18n="courses.back_classrooms")
else
a(href="/courses", data-i18n="courses.back_courses")
br
br
p
strong
if view.courseInstance.get('name')
span= view.courseInstance.get('name')
else if view.classroom.get('name')
span= view.classroom.get('name')
else
span(data-i18n='courses.unnamed_class')
p
// TODO: format this text all good and stuff
strong
if view.courseInstance.get('name')
span= view.courseInstance.get('name')
else if view.classroom.get('name')
span= view.classroom.get('name')
else
span(data-i18n='courses.unnamed_class')
if !view.owner.isNew() && view.getOwnerName() && view.courseInstance.get('name') != 'Single Player'
span.spl -
span.spl(data-i18n='courses.teacher')
span.spr :
span
strong= view.getOwnerName()
if !view.owner.isNew() && view.getOwnerName() && view.courseInstance.get('name') != 'Single Player'
span.spl -
span.spl(data-i18n='courses.teacher')
span.spr :
//a(href="/user/#{view.owner.id}") // Don't link to profiles until we improve them
span
strong= view.getOwnerName()
h1
| #{view.course.get('name')}
if view.courseComplete
span.spl -
span.spl(data-i18n='courses.complete')
span !
h1
| #{view.course.get('name')}
if view.courseComplete
span.spl -
span.spl(data-i18n='courses.complete')
span !
p
if view.courseInstance.get('description')
each line in view.courseInstance.get('description').split('\n')
div= line
p
if view.courseInstance.get('description')
each line in view.courseInstance.get('description').split('\n')
div= line
if view.courseComplete && !view.teacherMode
.jumbotron
.row
.col-md-6
if view.arenaLevel
a.btn.btn-lg.btn-success.btn-play-level(data-level-slug=view.arenaLevel.get('slug'), data-level-id=view.arenaLevel.get('original'))
h1
span(data-i18n='courses.arena')
span.spr :
span= view.arenaLevel.get('name')
p= view.arenaLevel.get('description').replace(/!\[.*?\)/, '')
else
a.btn.btn-lg.btn-success.disabled
h1(data-i18n='courses.arena_soon_title')
p
span.spr(data-i18n='courses.arena_soon_description')
span= view.course.get('name')
span .
.col-md-6
if view.nextCourseInstance && _.contains(view.nextCourseInstance.get('members'), me.id)
a.btn.btn-lg.btn-success(href="/courses/#{view.nextCourse.id}/#{view.nextCourseInstance.id}")
h1= view.nextCourse.get('name')
p= view.nextCourse.get('description')
else if view.nextCourse
a.btn.btn-lg.btn-success.disabled
h1= view.nextCourse.get('name')
p.text-uppercase
em(data-i18n='courses.not_enrolled1')
p(data-i18n='courses.not_enrolled2')
else
a.btn.btn-lg.btn-success(disabled=!view.nextCourse ? "disabled" : "")
h1(data-i18n='courses.next_course')
p.text-uppercase
em(data-i18n='courses.coming_soon1')
p(data-i18n='courses.coming_soon2')
.available-courses-title(data-i18n='courses.available_levels')
table.table.table-striped.table-condensed
thead
tr
th
th(data-i18n="clans.status")
th(data-i18n="common.type")
th(data-i18n="resources.level")
th(data-i18n="courses.concepts")
tbody
- var previousLevelCompleted = true;
- var lastLevelCompleted = view.getLastLevelCompleted();
- var passedLastCompletedLevel = !lastLevelCompleted;
- var levelCount = 0;
each level in view.levels.models
- var levelStatus = null;
- var levelNumber = view.classroom.getLevelNumber(level.get('original'), ++levelCount);
if view.userLevelStateMap[me.id]
- levelStatus = view.userLevelStateMap[me.id][level.get('original')]
.available-courses-title(data-i18n='courses.available_levels')
table.table.table-striped.table-condensed
thead
tr
td
if previousLevelCompleted || view.teacherMode || !passedLastCompletedLevel || levelStatus
- 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 });
if session
- 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='sharing.game')
else
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}. #{i18n(level.attributes, 'name').replace('Course: ', '')}
td
if view.levelConceptMap[level.get('original')]
each concept in view.course.get('concepts')
if view.levelConceptMap[level.get('original')][concept]
span.spr.concept(data-i18n="concepts." + concept)
if level.get('original') === lastLevelCompleted
- passedLastCompletedLevel = true
if !level.get('practice')
if view.userLevelStateMap[me.id]
- previousLevelCompleted = view.userLevelStateMap[me.id][level.get('original')] === 'complete'
else
- previousLevelCompleted = false
th
th(data-i18n="clans.status")
th(data-i18n="common.type")
th(data-i18n="resources.level")
th(data-i18n="courses.concepts")
tbody
- var previousLevelCompleted = true;
- var lastLevelCompleted = view.getLastLevelCompleted();
- var passedLastCompletedLevel = !lastLevelCompleted;
- var levelCount = 0;
each level in view.levels.models
- var levelStatus = null;
- var levelNumber = view.classroom.getLevelNumber(level.get('original'), ++levelCount);
if view.userLevelStateMap[me.id]
- levelStatus = view.userLevelStateMap[me.id][level.get('original')]
tr
td
if previousLevelCompleted || !passedLastCompletedLevel || levelStatus
- var i18nTag = level.isType('course-ladder') ? 'play.compete' : 'home.play';
button.btn.btn-forest.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 });
if session
- var url = '/play/' + level.get('type') + '-level/' + level.get('slug') + '/' + session.id + '?course=' + view.courseID;
a.btn.btn-gold.btn-view-project-level(href=url)
if level.isType('game-dev')
span(data-i18n='sharing.game')
else
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}. #{i18n(level.attributes, 'name').replace('Course: ', '')}
td
if view.levelConceptMap[level.get('original')]
each concept in view.course.get('concepts')
if view.levelConceptMap[level.get('original')][concept]
span.spr.concept(data-i18n="concepts." + concept)
if level.get('original') === lastLevelCompleted
- passedLastCompletedLevel = true
if !level.get('practice')
if view.userLevelStateMap[me.id]
- previousLevelCompleted = view.userLevelStateMap[me.id][level.get('original')] === 'complete'
else
- previousLevelCompleted = false

View file

@ -10,15 +10,16 @@ button.btn.btn-xs.btn-inverse.picoctf-hide#volume-button(title="Adjust volume")
button.btn.btn-xs.btn-inverse.picoctf-hide#music-button(title="Toggle Music")
span ♫
.scrubber
.scrubber-inner
.progress.secret#timeProgress
.progress-bar
.scrubber-handle
.popover.fade.top.in#timePopover
.arrow
h3.popover-title
.popover-content
if !view.options.level.isType('game-dev')
.scrubber
.scrubber-inner
.progress.secret#timeProgress
.progress-bar
.scrubber-handle
.popover.fade.top.in#timePopover
.arrow
h3.popover-title
.popover-content
.btn-group.dropup#playback-settings
button.btn.btn-xs.btn-inverse.toggle-fullscreen(title="Toggle fullscreen")

View file

@ -0,0 +1,17 @@
extends /templates/core/modal-base-flat
block modal-header-content
h3.text-center You beat the game!
block modal-body-content
.text-center Share this level so your friends and family can play it:
.share-row.text-center
input#copy-url-input.text-h4.semibold.form-control.input-lg(value=view.shareURL)
button#copy-url-btn.btn.btn-lg.btn-navy-alt
span(data-i18n='sharing.copy_url')
block modal-footer-content
.text-center
button#replay-game-btn.btn.btn-navy.btn-lg(data-dismiss="modal") Replay Game
a#play-more-codecombat-btn.btn.btn-navy.btn-lg(href="/") Play More CodeCombat

View file

@ -1,38 +1,58 @@
.container-fluid
.row
- var ready = !(view.state.get('errorMessage') || view.state.get('loading'))
.container-fluid.style-flat
#game-row.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
if view.state.get('errorMessage')
.alert.alert-danger= view.state.get('errorMessage')
#info-col.col-xs-3
.panel.panel-default
.panel-body.text-center
if view.state.get('errorMessage')
.alert.alert-danger= view.state.get('errorMessage')
else if view.state.get('loading')
h1.m-y-1(data-i18n="common.loading")
.progress
.progress-bar(style="width: #{view.state.get('progress')}")
if view.level.id && view.session.id
h3.m-y-1= view.level.get('name')
h4 Created by #{view.session.get('creatorName')}
hr
else
h1.m-y-1 Info
ul
li
b
span(data-i18n="play_level.level")
span= ': '
| #{view.level.get('name')}
if view.state.get('loading')
h1.m-y-1(data-i18n="common.loading")
.progress
.progress-bar(style="width: #{view.state.get('progress')}")
li
b
span(data-i18n="game_dev.creator")
span= ': '
| #{view.session.get('creatorName')}
if ready
h3 Goals
for goalName in view.state.get('goalNames')
p= goalName
- var playing = view.state.get('playing')
.m-y-3
if playing
button#play-btn.btn.btn-lg.btn-burgandy(data-i18n="play_level.restart")
else
button#play-btn.btn.btn-lg.btn-navy(data-i18n="common.play")
hr
h3 How to play:
p Use the mouse to control the hero!
p Click anywhere on the map to move to that location.
p Click on the ogres to attack them.
if ready
.panel-footer
- var playing = view.state.get('playing')
if playing
button#play-btn.btn.btn-lg.btn-burgandy.btn-block Restart Level
else
button#play-btn.btn.btn-lg.btn-forest.btn-block Play Level
#share-row.m-t-3
if ready
.panel.panel-default
#share-panel-body.panel-body
div#share-text-div.text-right
b(data-i18n='sharing.share_game')
input#copy-url-input.text-h4.semibold.form-control.input-lg(value=view.state.get('shareURL'))
div#copy-url-div
button#copy-url-btn.btn.btn-lg.btn-navy-alt
span(data-i18n='sharing.copy_url')
.panel-body
a#play-more-codecombat-btn.btn.btn-lg.btn-navy-alt.pull-right(href="/") Play More CodeCombat

View file

@ -0,0 +1,25 @@
if view.options.level.isType('game-dev')
button.btn.btn-lg.btn-illustrated.btn-success.game-dev-play-btn
span(data-i18n="common.play")
button.btn.btn-lg.btn-illustrated.btn-success.done-button.secret
span(data-i18n="play_level.done")
else
button.btn.btn-lg.btn-illustrated.cast-button(title=view.castVerbose())
span(data-i18n="play_level.tome_run_button_ran") Ran
if !view.observing
if view.mirror
.ladder-submission-view
else
button.btn.btn-lg.btn-illustrated.submit-button(title=view.castRealTimeVerbose())
span(data-i18n="play_level.tome_submit_button") Submit
span.spl.secret.submit-again-time
button.btn.btn-lg.btn-illustrated.btn-success.done-button.secret
span(data-i18n="play_level.done") Done
if view.autoSubmitsToLadder
.hidden
.ladder-submission-view

View file

@ -1,17 +0,0 @@
button.btn.btn-lg.btn-illustrated.cast-button(title=view.castVerbose())
span(data-i18n="play_level.tome_run_button_ran") Ran
if !view.observing
if view.mirror
.ladder-submission-view
else
button.btn.btn-lg.btn-illustrated.submit-button(title=view.castRealTimeVerbose())
span(data-i18n="play_level.tome_submit_button") Submit
span.spl.secret.submit-again-time
button.btn.btn-lg.btn-illustrated.btn-success.done-button.secret
span(data-i18n="play_level.done") Done
if view.autoSubmitsToLadder
.hidden
.ladder-submission-view

View file

@ -49,7 +49,20 @@ if view.showAds()
#level-dialogue-view
button.btn.btn-lg.btn-warning.banner.header-font#stop-real-time-playback-button(title="Stop real-time playback", data-i18n="play_level.skip") Skip
button.btn.btn-lg.btn-warning.banner.header-font#stop-real-time-playback-button(title="Stop real-time playback")
if view.level && view.level.isType('game-dev')
| Back to coding
else
span(data-i18n="play_level.skip")
#how-to-play-game-dev-panel.panel.panel-default.hide
.panel-heading
h3.panel-title How to play:
.panel-body
p Use the mouse to control the hero!
p Click anywhere on the map to move to that location.
p Click on the ogres to attack them.
.hints-view.hide

View file

@ -14,7 +14,6 @@ storage = require 'core/storage'
module.exports = class CourseDetailsView extends RootView
id: 'course-details-view'
template: template
teacherMode: false
memberSort: 'nameAsc'
events:
@ -24,7 +23,6 @@ module.exports = class CourseDetailsView extends RootView
constructor: (options, @courseID, @courseInstanceID) ->
super options
@ownedClassrooms = new Classrooms()
@courses = new Courses()
@course = new Course()
@levelSessions = new LevelSessions()
@ -34,7 +32,6 @@ module.exports = class CourseDetailsView extends RootView
@levels = new Levels()
@courseInstances = new CourseInstances()
@supermodel.trackRequest @ownedClassrooms.fetchMine({data: {project: '_id'}})
@supermodel.trackRequest(@courses.fetch().then(=>
@course = @courses.get(@courseID)
))
@ -42,8 +39,6 @@ module.exports = class CourseDetailsView extends RootView
@supermodel.trackRequest(@courseInstance.fetch().then(=>
return if @destroyed
@teacherMode = @courseInstance.get('ownerID') is me.id
@owner = new User({_id: @courseInstance.get('ownerID')})
@supermodel.trackRequest(@owner.fetch())

View file

@ -1,5 +1,5 @@
CocoView = require 'views/core/CocoView'
template = require 'templates/play/level/control_bar'
template = require 'templates/play/level/control-bar-view'
{me} = require 'core/auth'
Campaign = require 'models/Campaign'

View file

@ -103,6 +103,7 @@ module.exports = class LevelGoalsView extends CocoView
@updatePlacement()
onSurfacePlaybackEnded: ->
return if @level.isType('game-dev')
@playbackEnded = true
@updateHeight()
@$el.addClass 'brighter'
@ -140,7 +141,7 @@ module.exports = class LevelGoalsView extends CocoView
playToggleSound: (sound) =>
return if @destroyed
@playSound sound
@playSound sound unless @options.level.isType('game-dev')
@soundTimeout = null
onSetLetterbox: (e) ->

View file

@ -1,5 +1,5 @@
CocoView = require 'views/core/CocoView'
template = require 'templates/play/level/playback'
template = require 'templates/play/level/level-playback-view'
{me} = require 'core/auth'
module.exports = class LevelPlaybackView extends CocoView
@ -50,7 +50,7 @@ module.exports = class LevelPlaybackView extends CocoView
afterRender: ->
super()
@$progressScrubber = $('.scrubber .progress', @$el)
@hookUpScrubber()
@hookUpScrubber() unless @options.level.isType('game-dev')
@updateMusicButton()
$(window).on('resize', @onWindowResize)
ua = navigator.userAgent.toLowerCase()
@ -154,7 +154,7 @@ module.exports = class LevelPlaybackView extends CocoView
ended = button.hasClass 'ended'
changed = button.hasClass('playing') isnt @playing
button.toggleClass('playing', @playing and not ended).toggleClass('paused', not @playing and not ended)
@playSound (if @playing then 'playback-play' else 'playback-pause')
@playSound (if @playing then 'playback-play' else 'playback-pause') unless @options.level.isType('game-dev')
return # don't stripe the bar
bar = @$el.find '.scrubber .progress'
bar.toggleClass('progress-striped', @playing and not ended).toggleClass('active', @playing and not ended)

View file

@ -10,6 +10,10 @@ ThangType = require 'models/ThangType'
Level = require 'models/Level'
LevelSession = require 'models/LevelSession'
State = require 'models/State'
utils = require 'core/utils'
urls = require 'core/urls'
Course = require 'models/Course'
GameDevVictoryModal = require './modal/GameDevVictoryModal'
TEAM = 'humans'
@ -17,8 +21,13 @@ module.exports = class PlayGameDevLevelView extends RootView
id: 'play-game-dev-level-view'
template: require 'templates/play/level/play-game-dev-level-view'
subscriptions:
'god:new-world-created': 'onNewWorld'
events:
'click #play-btn': 'onClickPlayButton'
'click #copy-url-btn': 'onClickCopyURLButton'
'click #play-more-codecombat-btn': 'onClickPlayMoreCodeCombatButton'
initialize: (@options, @levelID, @sessionID) ->
@state = new State({
@ -32,9 +41,10 @@ module.exports = class PlayGameDevLevelView extends RootView
@session = new LevelSession()
@gameUIState = new GameUIState()
@courseID = @getQueryVariable 'course'
@god = new God({ @gameUIState })
@god = new God({ @gameUIState, indefiniteLength: true })
@levelLoader = new LevelLoader({ @supermodel, @levelID, @sessionID, observing: true, team: TEAM, @courseID })
@listenTo @state, 'change', _.debounce(-> @renderSelectors('#info-col'))
@supermodel.setMaxProgress 1 # Hack, why are we setting this to 0.2 in LevelLoader?
@listenTo @state, 'change', _.debounce @renderAllButCanvas
@levelLoader.loadWorldNecessities()
@ -50,6 +60,7 @@ module.exports = class PlayGameDevLevelView extends RootView
@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
@renderAllButCanvas()
@supermodel.finishLoading()
.then (supermodel) =>
@ -61,7 +72,9 @@ module.exports = class PlayGameDevLevelView extends RootView
thangTypes: @supermodel.getModels(ThangType)
levelType: @level.get('type', true)
@gameUIState
resizeStrategy: 'wrapper-size'
})
@listenTo @surface, 'resize', @onSurfaceResize
worldBounds = @world.getBounds()
bounds = [{x: worldBounds.left, y: worldBounds.top}, {x: worldBounds.right, y: worldBounds.bottom}]
@surface.camera.setBounds(bounds)
@ -70,18 +83,61 @@ module.exports = class PlayGameDevLevelView extends RootView
@scriptManager.initializeCamera()
@renderSelectors '#info-col'
@spells = @session.generateSpellsObject level: @level
@state.set('loading', false)
goalNames = (utils.i18n(goal, 'name') for goal in @goalManager.goals)
.catch ({message}) =>
console.error message
@state.set('errorMessage', message)
course = if @courseID then new Course({_id: @courseID}) else null
shareURL = urls.playDevLevel({@level, @session, course})
@state.set({
loading: false
goalNames
shareURL
})
@eventProperties = {
category: 'Play GameDev Level'
@courseID
sessionID: @session.id
levelID: @level.id
levelSlug: @level.get('slug')
}
window.tracker?.trackEvent 'Play GameDev Level - Load', @eventProperties, ['Mixpanel']
@god.createWorld(@spells, false, false, true)
.catch (e) =>
throw e if e.stack
@state.set('errorMessage', e.message)
onClickPlayButton: ->
@god.createWorld(@spells, false, true)
Backbone.Mediator.publish('playback:real-time-playback-started', {})
Backbone.Mediator.publish('level:set-playing', {playing: true})
action = if @state.get('playing') then 'Play GameDev Level - Restart Level' else 'Play GameDev Level - Start Level'
window.tracker?.trackEvent(action, @eventProperties, ['Mixpanel'])
@state.set('playing', true)
onClickCopyURLButton: ->
@$('#copy-url-input').val(@state.get('shareURL')).select()
@tryCopy()
window.tracker?.trackEvent('Play GameDev Level - Copy URL', @eventProperties, ['Mixpanel'])
onClickPlayMoreCodeCombatButton: ->
window.tracker?.trackEvent('Play GameDev Level - Click Play More CodeCombat', @eventProperties, ['Mixpanel'])
onSurfaceResize: ({height}) ->
@state.set('surfaceHeight', height)
renderAllButCanvas: ->
@renderSelectors('#info-col', '#share-row')
height = @state.get('surfaceHeight')
if height
@$el.find('#info-col').css('height', @state.get('surfaceHeight'))
onNewWorld: (e) ->
if @goalManager.checkOverallStatus() is 'success'
modal = new GameDevVictoryModal({ shareURL: @state.get('shareURL'), @eventProperties })
@openModalView(modal)
modal.once 'replay', @onClickPlayButton, @
destroy: ->
@levelLoader?.destroy()
@surface?.destroy()

View file

@ -142,7 +142,12 @@ module.exports = class PlayLevelView extends RootView
@listenTo @levelLoader, 'world-necessity-load-failed', @onWorldNecessityLoadFailed
onLevelLoaded: (e) ->
@god = new God({@gameUIState}) unless e.level.isType('web-dev')
return if @destroyed
unless e.level.isType('web-dev')
@god = new God({
@gameUIState
indefiniteLength: e.level.isType('game-dev')
})
@setupGod() if @waitingToSetUpGod
trackLevelLoadEnd: ->
@ -206,6 +211,7 @@ module.exports = class PlayLevelView extends RootView
@$el.addClass 'web-dev' # Hide some of the elements we won't be using
return
@world = @levelLoader.world
@$el.addClass 'game-dev' if @level.isType('game-dev')
@$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
@ -214,6 +220,7 @@ module.exports = class PlayLevelView extends RootView
@worldLoadFakeResources = [] # first element (0) is 1%, last (99) is 100%
for percent in [1 .. 100]
@worldLoadFakeResources.push @supermodel.addSomethingResource 1
@renderSelectors '#stop-real-time-playback-button'
onWorldLoadProgressChanged: (e) ->
return unless e.god is @god
@ -354,6 +361,7 @@ module.exports = class PlayLevelView extends RootView
levelType: @level.get('type', true)
stayVisible: @showAds()
@gameUIState
@level # TODO: change from levelType to level
}
@surface = new Surface(@world, normalSurface, webGLSurface, surfaceOptions)
worldBounds = @world.getBounds()
@ -626,10 +634,12 @@ module.exports = class PlayLevelView extends RootView
# Real-time playback
onRealTimePlaybackStarted: (e) ->
@$el.addClass('real-time').focus()
@$('#how-to-play-game-dev-panel').removeClass('hide') if @level.isType('game-dev')
@onWindowResize()
onRealTimePlaybackEnded: (e) ->
return unless @$el.hasClass 'real-time'
@$('#how-to-play-game-dev-panel').addClass('hide') if @level.isType('game-dev')
@$el.removeClass 'real-time'
@onWindowResize()
if @world.frames.length is @world.totalFrames and not @surface.countdownScreen?.showing

View file

@ -66,9 +66,9 @@ module.exports = class WebSurfaceView extends CocoView
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 }
wrappedStyles = deku.element('head', {}, styles)
wrappedScripts = deku.element('head', {}, scripts)
return { virtualDom, scripts: wrappedScripts, styles: wrappedStyles }
combineNodes: (type, nodes) ->
if _.any(nodes, (node) -> node.type isnt type)

View file

@ -108,9 +108,9 @@ module.exports = class CourseVictoryModal extends ModalView
onDone: ->
window.tracker?.trackEvent 'Play Level Victory Modal Done', category: 'Students', levelSlug: @level.get('slug'), ['Mixpanel']
if me.isSessionless()
link = "/teachers/courses"
link = '/teachers/courses'
else
link = "/courses/#{@courseID}/#{@courseInstanceID}"
link = '/courses'
application.router.navigate(link, {trigger: true})
onLadder: ->

View file

@ -0,0 +1,25 @@
ModalView = require 'views/core/ModalView'
category = 'Play GameDev Level'
module.exports = class GameDevVictoryModal extends ModalView
id: 'game-dev-victory-modal'
template: require 'templates/play/level/modal/game-dev-victory-modal'
events:
'click #replay-game-btn': 'onClickReplayButton'
'click #copy-url-btn': 'onClickCopyURLButton'
'click #play-more-codecombat-btn': 'onClickPlayMoreCodeCombatButton'
initialize: ({@shareURL, @eventProperties}) ->
onClickReplayButton: ->
@trigger 'replay'
onClickCopyURLButton: ->
@$('#copy-url-input').val(@shareURL).select()
@tryCopy()
window.tracker?.trackEvent('Play GameDev Victory Modal - Copy URL', @eventProperties, ['Mixpanel'])
onClickPlayMoreCodeCombatButton: ->
window.tracker?.trackEvent('Play GameDev Victory Modal - Click Play More CodeCombat', @eventProperties, ['Mixpanel'])

View file

@ -1,5 +1,6 @@
CocoView = require 'views/core/CocoView'
utils = require 'core/utils'
urls = require 'core/urls'
module.exports = class ProgressView extends CocoView
@ -25,8 +26,7 @@ module.exports = class ProgressView extends CocoView
@nextLevel.get('description', true) # Make sure the defaults are available
@nextLevelDescription = marked(utils.i18n(@nextLevel.attributesWithDefaults, 'description').replace(/!\[.*?\]\(.*?\)\n*/g, ''))
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
@shareURL = urls.playDevLevel({@level, @session, @course})
onClickDoneButton: ->
@trigger 'done'
@ -38,5 +38,19 @@ module.exports = class ProgressView extends CocoView
@trigger 'ladder'
onClickShareLevelButton: ->
if _.string.startsWith(@course.get('slug'), 'game-dev')
name = 'Student Game Dev - Copy URL'
category = 'GameDev'
else
name = 'Student Web Dev - Copy URL'
category = 'WebDev'
eventProperties = {
levelID: @level.id
levelSlug: @level.get('slug')
classroomID: @classroom.id
courseID: @course.id
category
}
window.tracker?.trackEvent name, eventProperties, ['MixPanel']
@$('#share-level-input').val(@shareURL).select()
@tryCopy()

View file

@ -1,5 +1,5 @@
CocoView = require 'views/core/CocoView'
template = require 'templates/play/level/tome/cast_button'
template = require 'templates/play/level/tome/cast-button-view'
{me} = require 'core/auth'
LadderSubmissionView = require 'views/play/common/LadderSubmissionView'
LevelSession = require 'models/LevelSession'
@ -12,6 +12,7 @@ module.exports = class CastButtonView extends CocoView
'click .cast-button': 'onCastButtonClick'
'click .submit-button': 'onCastRealTimeButtonClick'
'click .done-button': 'onDoneButtonClick'
'click .game-dev-play-btn': 'onClickGameDevPlayButton'
subscriptions:
'tome:spell-changed': 'onSpellChanged'
@ -74,6 +75,9 @@ module.exports = class CastButtonView extends CocoView
Backbone.Mediator.publish 'tome:manual-cast', {realTime: true}
@updateReplayability()
onClickGameDevPlayButton: ->
Backbone.Mediator.publish 'tome:manual-cast', {realTime: true}
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
@ -86,7 +90,7 @@ module.exports = class CastButtonView extends CocoView
return if e.preload
@casting = true
if @hasStartedCastingOnce # Don't play this sound the first time
@playSound 'cast', 0.5
@playSound 'cast', 0.5 unless @options.level.isType('game-dev')
@hasStartedCastingOnce = true
@updateCastButton()
@ -98,7 +102,7 @@ module.exports = class CastButtonView extends CocoView
onNewWorld: (e) ->
@casting = false
if @hasCastOnce # Don't play this sound the first time
@playSound 'cast-end', 0.5
@playSound 'cast-end', 0.5 unless @options.level.isType('game-dev')
# Worked great for live beginner tournaments, but probably annoying for asynchronous tournament mode.
myHeroID = if me.team is 'ogres' then 'Hero Placeholder 1' else 'Hero Placeholder'
if @autoSubmitsToLadder and not e.world.thangMap[myHeroID]?.errorsOut and not me.get('anonymous')
@ -113,7 +117,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') or @options.level.isType('web-dev')
if @options.level.get('hidesRealTimePlayback') or @options.level.isType('web-dev', 'game-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.

View file

@ -58,6 +58,7 @@ module.exports = class DocFormatter
when 'java' then 'hero'
when 'coffeescript' then '@'
else (if @options.useHero then 'hero' else 'this')
ownerName = 'game' if @options.level.isType('game-dev')
if @doc.type is 'function'
[docName, args] = @getDocNameAndArguments()
argNames = args.join ', '

View file

@ -85,9 +85,8 @@ module.exports = class SpellPaletteEntryView extends CocoView
Backbone.Mediator.publish 'tome:palette-pin-toggled', entry: @, pinned: @popoverPinned
onClick: (e) =>
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
if @options.level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder')
# Jiggle instead of pin for hero/course levels
jigglyPopover = $('.spell-palette-popover.popover')
jigglyPopover.addClass 'jiggling'
pauseJiggle = =>

View file

@ -69,6 +69,7 @@ module.exports = class SpellView extends CocoView
@highlightCurrentLine = _.throttle @highlightCurrentLine, 100
$(window).on 'resize', @onWindowResize
@observing = @session.get('creator') isnt me.id
afterRender: ->
super()
@createACE()
@ -93,6 +94,7 @@ module.exports = class SpellView extends CocoView
@aceSession.setUseWrapMode true
@aceSession.setNewLineMode 'unix'
@aceSession.setUseSoftTabs true
@aceSession.on 'changeAnnotation', @onChangeAnnotation
@ace.setTheme 'ace/theme/textmate'
@ace.setDisplayIndentGuides false
@ace.setShowPrintMargin false
@ -125,7 +127,7 @@ module.exports = class SpellView extends CocoView
addCommand
name: 'run-code'
bindKey: {win: 'Shift-Enter|Ctrl-Enter', mac: 'Shift-Enter|Command-Enter|Ctrl-Enter'}
exec: -> Backbone.Mediator.publish 'tome:manual-cast', {}
exec: => Backbone.Mediator.publish 'tome:manual-cast', {realTime: @options.level.isType('game-dev')}
unless @observing
addCommand
name: 'run-code-real-time'
@ -583,8 +585,8 @@ module.exports = class SpellView extends CocoView
# @addZatannaSnippets()
@highlightCurrentLine()
cast: (preload=false, realTime=false) ->
Backbone.Mediator.publish 'tome:cast-spell', spell: @spell, thang: @thang, preload: preload, realTime: realTime
cast: (preload=false, realTime=false, justBegin=false) ->
Backbone.Mediator.publish 'tome:cast-spell', { @spell, @thang, preload, realTime, justBegin }
notifySpellChanged: =>
return if @destroyed
@ -710,7 +712,7 @@ module.exports = class SpellView extends CocoView
@aceDoc.removeListener 'change', @onCodeChangeMetaHandler if @onCodeChangeMetaHandler
onSignificantChange = []
onAnyChange = [
_.debounce @updateAether, 500
_.debounce @updateAether, if @options.level.isType('game-dev') then 10 else 500
_.debounce @notifyEditingEnded, 1000
_.throttle @notifyEditingBegan, 250
_.throttle @notifySpellChanged, 300
@ -794,6 +796,18 @@ module.exports = class SpellView extends CocoView
else
finishUpdatingAether(aether)
# NOTE! Because this alone causes the doctype annotation to flicker,
# all info annotations have been hidden with CSS in spell.sass
# If we ever want info annotations back, we need to remove that.
#
# This function itself removes the unwanted annotations on a later tick.
onChangeAnnotation: (event, session) ->
unfilteredAnnotations = session.getAnnotations()
filteredAnnotations = _.remove unfilteredAnnotations, (annotation) ->
annotation.text is 'Start tag seen without seeing a doctype first. Expected e.g. <!DOCTYPE html>.'
if filteredAnnotations.length < unfilteredAnnotations.length
session.setAnnotations(filteredAnnotations)
# Clear annotations and highlights generated by Aether, but not by the ACE worker
clearAetherDisplay: ->
problem.destroy() for problem in @problems
@ -873,13 +887,18 @@ module.exports = class SpellView extends CocoView
# - Go after specified delay if a) and not b) or c)
guessWhetherFinished: (aether) ->
valid = not aether.getAllProblems().length
return unless valid
cursorPosition = @ace.getCursorPosition()
currentLine = _.string.rtrim(@aceDoc.$lines[cursorPosition.row].replace(@singleLineCommentRegex(), '')) # trim // unless inside "
endOfLine = cursorPosition.column >= currentLine.length # just typed a semicolon or brace, for example
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 not incompleteThis and @options.level.isType('game-dev')
# TODO: Improve gamedev autocast speed
@spell.transpile @getSource()
@cast(false, false, true)
else if (endOfLine or beginningOfLine) and not incompleteThis
@preload()
singleLineCommentRegex: ->
@ -1270,3 +1289,4 @@ commentStarts =
coffeescript: '#'
lua: '--'
java: '//'
html: '<!--'

View file

@ -143,9 +143,9 @@ module.exports = class TomeView extends CocoView
onCastSpell: (e) ->
# A single spell is cast.
@cast e?.preload, e?.realTime
@cast e?.preload, e?.realTime, e?.justBegin
cast: (preload=false, realTime=false) ->
cast: (preload=false, realTime=false, justBegin=false) ->
return if @options.level.isType('web-dev')
sessionState = @options.session.get('state') ? {}
if realTime
@ -156,7 +156,17 @@ module.exports = class TomeView extends CocoView
difficulty = sessionState.difficulty ? 0
if @options.observing
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
Backbone.Mediator.publish 'tome:cast-spells', {
@spells,
preload,
realTime,
justBegin,
difficulty,
submissionCount: sessionState.submissionCount ? 0,
flagHistory: sessionState.flagHistory ? [],
god: @options.god,
fixedSeed: @options.fixedSeed
}
onClick: (e) ->
Backbone.Mediator.publish 'tome:focus-editor', {} unless $(e.target).parents('.popover').length