Merge branch 'feature/campaign-editor'

This commit is contained in:
Nick Winter 2014-12-27 11:03:51 -08:00
commit b4341ad46c
24 changed files with 2450 additions and 10 deletions

View file

@ -66,6 +66,7 @@ module.exports = class CocoRouter extends Backbone.Router
'editor/level/:levelID': go('editor/level/LevelEditView') 'editor/level/:levelID': go('editor/level/LevelEditView')
'editor/thang': go('editor/thang/ThangTypeSearchView') 'editor/thang': go('editor/thang/ThangTypeSearchView')
'editor/thang/:thangID': go('editor/thang/ThangTypeEditView') 'editor/thang/:thangID': go('editor/thang/ThangTypeEditView')
'editor/campaign/:campaignID': go('editor/campaign/CampaignEditorView')
'employers': go('EmployersView') 'employers': go('EmployersView')
@ -84,7 +85,7 @@ module.exports = class CocoRouter extends Backbone.Router
'multiplayer': go('MultiplayerView') 'multiplayer': go('MultiplayerView')
'play-old': go('play/MainPlayView') # This used to be 'play'. 'play-old': go('play/MainPlayView') # This used to be 'play'.
'play': go('play/WorldMapView') 'play': go('play/CampaignView')
'play/ladder/:levelID': go('ladder/LadderView') 'play/ladder/:levelID': go('ladder/LadderView')
'play/ladder': go('ladder/MainLadderView') 'play/ladder': go('ladder/MainLadderView')
'play/level/:levelID': go('play/level/PlayLevelView') 'play/level/:levelID': go('play/level/PlayLevelView')

View file

@ -313,7 +313,7 @@ class InternationalizationNode extends TreemaNode.nodeMap.object
class LatestVersionCollection extends CocoCollection class LatestVersionCollection extends CocoCollection
class LatestVersionReferenceNode extends TreemaNode module.exports.LatestVersionReferenceNode = class LatestVersionReferenceNode extends TreemaNode
searchValueTemplate: '<input placeholder="Search" /><div class="treema-search-results"></div>' searchValueTemplate: '<input placeholder="Search" /><div class="treema-search-results"></div>'
valueClass: 'treema-latest-version' valueClass: 'treema-latest-version'
url: '/db/article' url: '/db/article'
@ -383,7 +383,11 @@ class LatestVersionReferenceNode extends TreemaNode
m = CocoModel.getReferencedModel(@getData(), @workingSchema) m = CocoModel.getReferencedModel(@getData(), @workingSchema)
data = @getData() data = @getData()
if _.isString data # LatestVersionOriginalReferenceNode just uses original if _.isString data # LatestVersionOriginalReferenceNode just uses original
m = @settings.supermodel.getModelByOriginal(m.constructor, data) if m.schema().properties.version
m = @settings.supermodel.getModelByOriginal(m.constructor, data)
else
# get by id
m = @settings.supermodel.getModel(m.constructor, data)
else else
m = @settings.supermodel.getModelByOriginalAndMajorVersion(m.constructor, data.original, data.majorVersion) m = @settings.supermodel.getModelByOriginalAndMajorVersion(m.constructor, data.original, data.majorVersion)
if @instance and not m if @instance and not m
@ -434,7 +438,7 @@ class LatestVersionReferenceNode extends TreemaNode
selected = @getSelectedResultEl() selected = @getSelectedResultEl()
return not selected.length return not selected.length
class LatestVersionOriginalReferenceNode extends LatestVersionReferenceNode module.exports.LatestVersionOriginalReferenceNode = class LatestVersionOriginalReferenceNode extends LatestVersionReferenceNode
# Just for saving the original, not the major version. # Just for saving the original, not the major version.
saveChanges: -> saveChanges: ->
selected = @getSelectedResultEl() selected = @getSelectedResultEl()
@ -443,6 +447,15 @@ class LatestVersionOriginalReferenceNode extends LatestVersionReferenceNode
@data = fullValue.attributes.original @data = fullValue.attributes.original
@instance = fullValue @instance = fullValue
module.exports.IDReferenceNode = class IDReferenceNode extends LatestVersionReferenceNode
# Just for saving the _id
saveChanges: ->
selected = @getSelectedResultEl()
return unless selected.length
fullValue = selected.data('value')
@data = fullValue.attributes._id
@instance = fullValue
class LevelComponentReferenceNode extends LatestVersionReferenceNode class LevelComponentReferenceNode extends LatestVersionReferenceNode
# HACK: this list of properties is needed by the thang components edit view and config views. # HACK: this list of properties is needed by the thang components edit view and config views.
# need a better way to specify this, or keep the search models from bleeding into those # need a better way to specify this, or keep the search models from bleeding into those

View file

@ -0,0 +1,10 @@
CocoModel = require './CocoModel'
schema = require 'schemas/models/campaign.schema'
module.exports = class Campaign extends CocoModel
@className: 'Campaign'
@schema: schema
urlRoot: '/db/campaign'
saveBackups: true
@denormalizedLevelProperties: _.keys(_.omit(schema.properties.levels.additionalProperties.properties, ['unlocks', 'position', 'rewards']))
@denormalizedCampaignProperties: ['name', 'i18n', 'description', 'slug']

View file

@ -0,0 +1,121 @@
c = require './../schemas'
CampaignSchema = c.object()
c.extendNamedProperties CampaignSchema # name first
_.extend CampaignSchema.properties, {
i18n: {type: 'object', title: 'i18n', format: 'i18n', props: ['name', 'body']}
ambientSound: c.object {},
mp3: { type: 'string', format: 'sound-file' }
ogg: { type: 'string', format: 'sound-file' }
backgroundImage: c.array {}, {
type: 'object'
additionalProperties: false
properties: {
image: { type: 'string', format: 'image-file' }
width: { type: 'number' }
}
}
backgroundColor: { type: 'string' }
backgroundColorTransparent: { type: 'string' }
adjacentCampaigns: { type: 'object', format: 'campaigns', additionalProperties: {
title: 'Campaign'
type: 'object'
format: 'campaign'
properties: {
#- denormalized from other Campaigns, either updated automatically or fetched dynamically
id: { type: 'string', format: 'hidden' }
name: { type: 'string', format: 'hidden' }
description: { type: 'string', format: 'hidden' }
i18n: { type: 'object', format: 'hidden' }
slug: { type: 'string', format: 'hidden' }
#- normal properties
position: c.point2d()
rotation: { type: 'number', format: 'degrees' }
color: { type: 'string' }
showIfUnlocked: { type: 'string', links: [{rel: 'db', href: '/db/level/{($)}/version'}], format: 'latest-version-original-reference' }
}
}}
levels: { type: 'object', format: 'levels', additionalProperties: {
title: 'Level'
type: 'object'
format: 'level'
additionalProperties: false
# key is the original property
properties: {
#- denormalized from Level
name: { type: 'string', format: 'hidden' }
description: { type: 'string', format: 'hidden' }
requiresSubscription: { type: 'boolean' }
type: {'enum': ['campaign', 'ladder', 'ladder-tutorial', 'hero', 'hero-ladder', 'hero-coop']}
slug: { type: 'string', format: 'hidden' }
original: { type: 'string', format: 'hidden' }
adventurer: { type: 'boolean' }
practice: { type: 'boolean' }
disableSpaces: { type: 'boolean' }
hidesSubmitUntilRun: { type: 'boolean' }
hidesPlayButton: { type: 'boolean' }
hidesRunShortcut: { type: 'boolean' }
hidesHUD: { type: 'boolean' }
hidesSay: { type: 'boolean' }
hidesCodeToolbar: { type: 'boolean' }
hidesRealTimePlayback: { type: 'boolean' }
backspaceThrottle: { type: 'boolean' }
lockDefaultCode: { type: 'boolean' }
moveRightLoopSnippet: { type: 'boolean' }
realTimeSpeedFactor: { type: 'number' }
autocompleteFontSizePx: { type: 'number' }
requiredCode: c.array {}, {
type: 'string'
}
suspectCode: c.array {}, {
type: 'object'
properties: {
name: { type: 'string' }
pattern: { type: 'string' }
}
}
requiredGear: { type: 'object', additionalProperties: {
type: 'array'
items: { type: 'string', links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], format: 'latest-version-original-reference' }
}}
restrictedGear: { type: 'object', additionalProperties: {
type: 'array'
items: { type: 'string', links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], format: 'latest-version-original-reference' }
}}
allowedHeroes: { type: 'array', items: {
type: 'string', links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], format: 'latest-version-original-reference'
}}
#- denormalized from Achievements
rewards: { type: 'array', items: {
type: 'object'
additionalProperties: false
properties:
achievement: { type: 'string', links: [{rel: 'db', href: '/db/achievement/{{$}}'}], format: 'achievement' }
item: { type: 'string', links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], format: 'latest-version-original-reference' }
hero: { type: 'string', links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], format: 'latest-version-original-reference' }
level: { type: 'string', links: [{rel: 'db', href: '/db/level/{($)}/version'}], format: 'latest-version-original-reference' }
type: { enum: ['heroes', 'items', 'levels'] }
}}
#- normal properties
position: c.point2d()
}
}}
}
c.extendBasicProperties CampaignSchema, 'campaign'
c.extendTranslationCoverageProperties CampaignSchema
module.exports = CampaignSchema

View file

@ -296,6 +296,42 @@ _.extend LevelSchema.properties,
requiresSubscription: {title: 'Requires Subscription', description: 'Whether this level is available to subscribers only.', type: 'boolean'} requiresSubscription: {title: 'Requires Subscription', description: 'Whether this level is available to subscribers only.', type: 'boolean'}
tasks: c.array {title: 'Tasks', description: 'Tasks to be completed for this level.', default: (name: t for t in defaultTasks)}, c.task tasks: c.array {title: 'Tasks', description: 'Tasks to be completed for this level.', default: (name: t for t in defaultTasks)}, c.task
# Admin flags
adventurer: { type: 'boolean' }
practice: { type: 'boolean' }
disableSpaces: { type: 'boolean' }
hidesSubmitUntilRun: { type: 'boolean' }
hidesPlayButton: { type: 'boolean' }
hidesRunShortcut: { type: 'boolean' }
hidesHUD: { type: 'boolean' }
hidesSay: { type: 'boolean' }
hidesCodeToolbar: { type: 'boolean' }
hidesRealTimePlayback: { type: 'boolean' }
backspaceThrottle: { type: 'boolean' }
lockDefaultCode: { type: 'boolean' }
moveRightLoopSnippet: { type: 'boolean' }
realTimeSpeedFactor: { type: 'number' }
autocompleteFontSizePx: { type: 'number' }
requiredCode: c.array {}, {
type: 'string'
}
suspectCode: c.array {}, {
type: 'object'
properties: {
name: { type: 'string' }
pattern: { type: 'string' }
}
}
requiredGear: { type: 'object', additionalProperties: {
type: 'string'
}}
restrictedGear: { type: 'object', additionalProperties: {
type: 'string'
}}
allowedHeroes: { type: 'array', items: {
type: 'string'
}}
c.extendBasicProperties LevelSchema, 'level' c.extendBasicProperties LevelSchema, 'level'
c.extendSearchableProperties LevelSchema c.extendSearchableProperties LevelSchema
c.extendVersionedProperties LevelSchema, 'level' c.extendVersionedProperties LevelSchema, 'level'

View file

@ -0,0 +1,28 @@
#campaign-editor-view
#left-column
position: absolute
top: 0
bottom: 0
left: 0
width: 25%
margin-right: 1200px
.treema-root
max-height: 100%
overflow: scroll
#right-column
position: absolute
top: 0
bottom: 0
right: 0
width: 75%
#campaign-level-view
position: absolute
top: 0
left: 0
right: 0
bottom: 0
background-color: white
z-index: 3

View file

@ -0,0 +1,495 @@
@import "app/styles/mixins"
@import "app/styles/bootstrap/variables"
$mapHeight: 1536
$forestMapWidth: 2500
$dungeonMapWidth: 2350
$desertMapWidth: 2350
$desertMapSeaBackground: rgba(113, 186, 208, 1)
$desertMapSeaBackgroundTransparent: rgba(113, 186, 208, 0)
$forestMapSeaBackground: rgba(113, 186, 208, 1)
$forestMapSeaBackgroundTransparent: rgba(113, 186, 208, 0)
$dungeonMapCaveBackground: rgba(68, 54, 45, 1)
$dungeonMapCaveBackgroundTransparent: rgba(68, 54, 45, 0)
$levelDotWidth: 2%
$levelDotHeight: $levelDotWidth * $forestMapWidth / $mapHeight
$levelDotZ: $levelDotHeight * 0.25
$levelDotHoverZ: $levelDotZ * 2
$levelDotShadowWidth: 0.8 * $levelDotWidth
$levelDotShadowHeight: 0.8 * $levelDotHeight
$levelClickRadius: 40px
$gameControlSize: 80px
$gameControlMargin: 30px
+keyframes(levelStartedPulse)
from
@include box-shadow(0px 0px 4px #333)
margin-bottom: -$levelDotHeight / 3 + $levelDotZ
50%
@include box-shadow(0px 0px 22px skyblue)
margin-bottom: -$levelDotHeight / 3 + ($levelDotHoverZ + $levelDotZ) / 2
to
@include box-shadow(0px 0px 4px #333)
margin-bottom: -$levelDotHeight / 3 + $levelDotZ
#campaign-view
width: 100%
height: 100%
position: absolute
.gradient
position: absolute
z-index: 0
&.horizontal-gradient
left: 0
right: 0
height: 3%
&.vertical-gradient
top: 0
bottom: 0
width: 3%
&.top-gradient
top: 0
&.right-gradient
right: 0
&.bottom-gradient
bottom: 0
&.left-gradient
left: 0
&.desert
background-color: $desertMapSeaBackground
.top-gradient
background: linear-gradient(to bottom, $desertMapSeaBackground 0%, $desertMapSeaBackgroundTransparent 100%)
.right-gradient
background: linear-gradient(to left, $desertMapSeaBackground 0%, $desertMapSeaBackgroundTransparent 100%)
.bottom-gradient
background: linear-gradient(to top, $desertMapSeaBackground 0%, $desertMapSeaBackgroundTransparent 100%)
.left-gradient
background: linear-gradient(to right, $desertMapSeaBackground 0%, $desertMapSeaBackgroundTransparent 100%)
&.forest
background-color: $forestMapSeaBackground
.top-gradient
background: linear-gradient(to bottom, $forestMapSeaBackground 0%, $forestMapSeaBackgroundTransparent 100%)
.right-gradient
background: linear-gradient(to left, $forestMapSeaBackground 0%, $forestMapSeaBackgroundTransparent 100%)
.bottom-gradient
background: linear-gradient(to top, $forestMapSeaBackground 0%, $forestMapSeaBackgroundTransparent 100%)
.left-gradient
background: linear-gradient(to right, $forestMapSeaBackground 0%, $forestMapSeaBackgroundTransparent 100%)
&.dungeon
background-color: $dungeonMapCaveBackground
.top-gradient
background: linear-gradient(to bottom, $dungeonMapCaveBackground 0%, $dungeonMapCaveBackgroundTransparent 100%)
.right-gradient
background: linear-gradient(to left, $dungeonMapCaveBackground 0%, $dungeonMapCaveBackgroundTransparent 100%)
.bottom-gradient
background: linear-gradient(to top, $dungeonMapCaveBackground 0%, $dungeonMapCaveBackgroundTransparent 100%)
.left-gradient
background: linear-gradient(to right, $dungeonMapCaveBackground 0%, $dungeonMapCaveBackgroundTransparent 100%)
.map
position: relative
.map-background
width: 100%
height: 100%
background-size: 100%
@include user-select(none)
&.map-dungeon
background-image: url('/images/pages/play/map_dungeon_1920.jpg')
@media screen and ( max-width: 1366px )
background-image: url('/images/pages/play/map_dungeon_1366.jpg')
&.map-forest
background-image: url('/images/pages/play/map_forest_1920.jpg')
@media screen and ( max-width: 1366px )
background-image: url('/images/pages/play/map_forest_1366.jpg')
&.map-desert
background-image: url('/images/pages/play/map_desert_1920.jpg')
@media screen and ( max-width: 1366px )
background-image: url('/images/pages/play/map_desert_1366.jpg')
.level, .level-shadow
position: absolute
border-radius: 100%
-webkit-transform: scaleY(0.75)
transform: scaleY(0.75)
.level
z-index: 2
width: $levelDotWidth
height: $levelDotHeight
margin-left: -0.5 * $levelDotWidth
margin-bottom: -$levelDotHeight / 3 + $levelDotZ
border: 2px groove white
@include transition(margin-bottom 0.5s ease)
&.disabled, &.locked
background-image: url(/images/pages/game-menu/lock.png)
background-size: 75%
background-repeat: no-repeat
background-position: 50% 50%
opacity: 0.7
a
cursor: default
&.next
width: 2 * $levelDotWidth
height: 2 * $levelDotHeight
margin-left: -0.5 * 2 * $levelDotWidth
margin-bottom: -2 * $levelDotHeight / 3 + 2 * $levelDotZ
&.started, &.next
border: 3px solid lightgreen
@include box-shadow(0px 0px 35px skyblue)
// Would be cool, but kills performance, since we have to re-render all the time.
//&:not(:hover)
// -webkit-animation-name: levelStartedPulse
// -webkit-animation-duration: 3s
// -webkit-animation-iteration-count: infinite
&.complete
border: 3px solid gold
@include box-shadow(0px 0px 35px skyblue)
img.banner
position: absolute
bottom: 38%
left: -50%
width: 200%
pointer-events: none
img.star
width: 100%
bottom: 7%
position: absolute
pointer-events: none
.glyphicon-star
position: absolute
color: lightblue
font-size: 21px
left: 1.5px
&.started .glyphicon-star
left: 0.5px
img.hero-portrait
width: 120%
position: absolute
bottom: 75%
left: 75%
border: 1px solid black
border-radius: 100%
background: white
.level-shadow
z-index: 1
width: $levelDotShadowWidth
height: $levelDotShadowHeight
margin-left: -0.5 * $levelDotShadowWidth
margin-bottom: -$levelDotShadowHeight / 3
background-color: black
@include box-shadow(0px 0px 10px black)
@include opacity(0.75)
&.next
width: 2 * $levelDotShadowWidth
height: 2 * $levelDotShadowHeight
margin-left: -0.5 * 2 * $levelDotShadowWidth
margin-bottom: -2 * $levelDotShadowHeight / 3
.level:hover
// TODO: This rotate stops Firefox from flickering, but also disables the scaleY(0.75)
// TODO: The dot looks like it's jumping.
// TODO: -moz-transform: scaleY(0.75) didn't do anything
// TODO: Does not break Chrome's oval.
-moz-transform: rotate(0)
margin-bottom: -$levelDotHeight / 3 + $levelDotHoverZ
@include box-shadow(0px 0px 35px skyblue)
&.next
margin-bottom: -2 * $levelDotHeight / 3 + 2 * $levelDotHoverZ
.level
a
display: block
padding: $levelClickRadius
margin-left: -0.5 * $levelClickRadius
margin-top: -0.5 * $levelClickRadius
border-radius: $levelClickRadius
&.next a
padding: 2 * $levelClickRadius
margin-left: 2 * -0.5 * $levelClickRadius
margin-top: 2 * -0.5 * $levelClickRadius
border-radius: 2 * $levelClickRadius
.tooltip
z-index: 2
.level-info-container
display: none
position: absolute
z-index: 3
padding: 10px
border-width: 16px 12px
// Using modernizr-mixin for compat detection
@include yep(borderimage)
border-style: solid
border-image: url(/images/level/popover_border_background.png) 16 12 fill round
@include nope(borderimage)
background-color: rgb(247, 242, 218)
.level-info.complete h3:after
content: " - Complete!"
color: green
.level-info.started h3:after
content: " - Started"
color: desaturate(green, 50%)
.level-info
h3
margin-top: 0
margin-bottom: 0px
.level-description
color: black
text-shadow: 0 1px 0 white
.campaign-label
text-shadow: 0 1px 0 white
.start-level
display: block
margin: 10px auto 0 auto
width: 200px
.campaign-switch
color: purple
position: absolute
z-index: 1
font-size: 2vw
text-shadow: 0 0 0.3vw white, 0 0 0.3vw white
&:hover
text-decoration: none
&#desert-link
left: 90%
top: 18.5%
transform: scaleY(-1.5) scaleX(1.5)
&#forest-back-link
left: 2%
top: 70.5%
transform: rotate(216deg)
&#forest-link
left: 94.5%
top: 7%
transform: rotate(-35deg)
&#dungeon-link
left: 9%
top: 54.5%
transform: rotate(180deg)
color: fuchsia
.game-controls
position: absolute
right: 1%
bottom: 1%
z-index: 3
.btn
&:not(:first-child)
margin-left: $gameControlMargin
width: $gameControlSize
height: $gameControlSize
background: url(/images/pages/play/menu_icons.png) no-repeat
position: relative
img
position: absolute
left: 0
top: 0
width: 100%
height: 100%
background-size: cover
@include transition(0.5s ease)
@include box-shadow(2px 2px 4px black)
border: 0
border-radius: 12px
// IE9 shows a blank white button with this MS gradient filter in place
filter: none
&:hover
@include box-shadow(0 0 12px #bbf)
&:active
@include box-shadow(0 0 20px white)
&.heroes
background-position: (-1 * $gameControlSize) 0px
&.achievements
background-position: (-2 * $gameControlSize) 0px
&.account
background-position: (-3 * $gameControlSize) 0px
&.settings
background-position: (-4 * $gameControlSize) 0px
&.gems
background-position: (-5 * $gameControlSize) 0px
.tooltip
font-size: 24px
.tooltip-arrow
display: none
.user-status
position: absolute
bottom: 16px
left: 8px
text-align: center
font-size: 24px
color: white
text-shadow: 0px 2px 1px black, 0px -2px 1px black, -2px 0px 1px black, 2px 0px 1px black
height: 32px
line-height: 32px
.user-status-line
position: relative
button.btn.btn-illustrated
margin-left: 10px
min-width: 90px
height: 32px
color: white
.gem, .player-level-icon, .player-hero-icon
position: absolute
top: 1px
#gems-count
margin-left: 40px
.player-level
margin-left: 34px
.player-name
margin-left: 45px
$spriteSheetSize: 30px
.player-level-icon, .player-hero-icon
background: transparent url(/images/pages/play/play-spritesheet.png)
background-size: cover
background-position: (-2 * $spriteSheetSize) 0
display: inline-block
width: 30px
height: 30px
margin: 0px 2px
.player-hero-icon
margin-left: 10px
background-position: (-4 * $spriteSheetSize) 0
&.knight
background-position: (-5 * $spriteSheetSize) 0
&.librarian
background-position: (-6 * $spriteSheetSize) 0
&.ninja
background-position: (-7 * $spriteSheetSize) 0
&.potion-master
background-position: (-8 * $spriteSheetSize) 0
&.samurai
background-position: (-9 * $spriteSheetSize) 0
&.trapper
background-position: (-10 * $spriteSheetSize) 0
&.forest-archer
background-position: (-11 * $spriteSheetSize) 0
&.sorcerer
background-position: (-12 * $spriteSheetSize) 0
#volume-button
position: absolute
left: 1%
top: 1%
padding: 3px 8px
@include opacity(0.75)
&:hover
@include opacity(1.0)
.glyphicon
display: none
font-size: 32px
&.vol-up .glyphicon.glyphicon-volume-up
display: inline-block
&.vol-off .glyphicon.glyphicon-volume-off
display: inline-block
@include opacity(0.50)
&:hover
@include opacity(0.75)
&.vol-down .glyphicon.glyphicon-volume-down
display: inline-block
#campaign-status
position: absolute
left: 0
top: 15px
width: 100%
margin: 0
text-align: center
color: rgb(254,188,68)
font-size: 30px
text-shadow: black 2px 2px 0, black -2px -2px 0, black 2px -2px 0, black -2px 2px 0, black 2px 0px 0, black 0px -2px 0, black -2px 0px 0, black 0px 2px 0
body:not(.ipad) #campaign-view
.level-info-container
pointer-events: none
body.ipad #campaign-view
// iPad only supports up to Kithgard Gates for now.
.campaign-switch
display: none
.old-levels
display: none

View file

@ -0,0 +1,44 @@
extends /templates/base
block header
if campaign.loading
nav.navbar.navbar-default(role='navigation')#campaign-editor-top-nav
.container-fluid
ul.nav.navbar-nav
li
a(href="/")
span.glyphicon-home.glyphicon
else
nav.navbar.navbar-default(role='navigation')#campaign-editor-top-nav
ul.nav.navbar-nav
li
a(href="/")
span.glyphicon-home.glyphicon
ul.nav.navbar-nav.navbar-right
if me.isAdmin()
li#save-button
a
span.glyphicon-floppy-disk.glyphicon
li.dropdown
a(data-toggle='dropdown')
span.glyphicon-chevron-down.glyphicon
ul.dropdown-menu
li.dropdown-header Actions
li(class=anonymous ? "disabled": "")
a(data-toggle="coco-modal", data-target="modal/RevertModal", data-i18n="editor.revert")#revert-button Revert
li(class=anonymous ? "disabled": "")
a(data-i18n="editor.pop_i18n")#pop-level-i18n-button Populate i18n
li.divider
li.dropdown-header Info
block outer_content
.outer-content
#left-column
#campaign-treema
#right-column
#campaign-view
#campaign-level-view.hidden
block footer

View file

@ -0,0 +1,4 @@
.jumbotron
.button.close(type="button", aria-hidden="true") &times;
h1= level.get('name')
p= level.get('description')

View file

@ -0,0 +1,21 @@
extends /templates/core/modal-base
block modal-header-content
h3 Save Changes to Campaign
block modal-body-content
if !modelsToSave.models.length
.alert.alert-info No changes
for model in modelsToSave.models
.panel.panel-default
.panel-heading
span.panel-title.spr= model.get('name')
span.text-muted= model.constructor.className
.panel-body
.delta-view(data-model-id=model.id)
block modal-footer
.modal-footer
button(data-dismiss="modal", data-i18n="common.cancel").btn Cancel
button.btn.btn-primary#save-button Save

View file

@ -0,0 +1,99 @@
.map
.gradient.horizontal-gradient.top-gradient
.gradient.vertical-gradient.right-gradient
.gradient.horizontal-gradient.bottom-gradient
.gradient.vertical-gradient.left-gradient
.map-background(class="map-"+mapType alt="", draggable="false")
each level in levels
if !level.hidden
- var next = nextLevel && level.slug === nextLevel;
div(style="left: #{level.position.x}%; bottom: #{level.position.y}%; background-color: #{level.color}", class="level" + (next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + levelStatusMap[level.original] || "", data-level-id=level.original, title=level.name + (level.disabled ? ' (Coming Soon to Adventurers)' : ''))
if level.unlocksHero && !level.unlockedHero
img.hero-portrait(src=level.unlocksHero.img)
a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.original}", disabled=level.disabled, data-level-id=level.original, data-level-path=level.levelPath || 'level', data-level-name=level.name)
if level.requiresSubscription
img.star(src="/images/pages/play/star.png")
if levelStatusMap[level.original] === 'complete'
img.banner(src="/images/pages/play/level-banner-complete.png")
if levelStatusMap[level.original] === 'started'
img.banner(src="/images/pages/play/level-banner-started.png")
div(style="left: #{level.position.x}%; bottom: #{level.position.y}%", class="level-shadow" + (next ? " next" : "") + " " + levelStatusMap[level.original] || "")
.level-info-container(data-level-id=level.original, data-level-path=level.levelPath || 'level', data-level-name=level.name)
div(class="level-info " + (levelStatusMap[level.original] || ""))
h3= level.name + (level.disabled ? " (Coming soon!)" : (level.locked ? " (Locked)" : ""))
.level-description= level.description
if level.disabled
p
span.spr(data-i18n="play.awaiting_levels_adventurer_prefix") We release five levels per week.
a.spr(href="/contribute/adventurer")
strong(data-i18n="play.awaiting_levels_adventurer") Sign up as an Adventurer
span.spl(data-i18n="play.awaiting_levels_adventurer_suffix") to be the first to play new levels.
- var playCount = levelPlayCountMap[level.original]
if playCount && playCount.sessions > 20
div
span.spr #{playCount.sessions}
span(data-i18n="play.players") players
span.spr , #{Math.round(playCount.playtime / 3600)}
span(data-i18n="play.hours_played") hours played
.campaign-label= campaign.get('name')
if isIPadApp && !level.disabled && !level.locked
button.btn.btn-success.btn-lg.start-level(data-i18n="common.play") Play
for adjacentCampaign in adjacentCampaigns
a
span.glyphicon.glyphicon-share-alt.campaign-switch(href="/play/"+adjacentCampaign.slug, style=adjacentCampaign.style, title=adjacentCampaign.name, data-campaign-id=adjacentCampaign.id)
.game-controls.header-font
button.btn.items(data-toggle='coco-modal', data-target='play/modal/PlayItemsModal', data-i18n="[title]play.items")
button.btn.heroes(data-toggle='coco-modal', data-target='play/modal/PlayHeroesModal', data-i18n="[title]play.heroes")
button.btn.achievements(data-toggle='coco-modal', data-target='play/modal/PlayAchievementsModal', data-i18n="[title]play.achievements")
if me.get('anonymous') === false || me.get('iosIdentifierForVendor') || isIPadApp
button.btn.gems(data-toggle='coco-modal', data-target='play/modal/BuyGemsModal', data-i18n="[title]play.buy_gems")
if me.isAdmin()
button.btn.account(data-toggle='coco-modal', data-target='play/modal/PlayAccountModal', data-i18n="[title]play.account")
button.btn.settings(data-toggle='coco-modal', data-target='play/modal/PlaySettingsModal', data-i18n="[title]play.settings")
else if me.get('anonymous', true)
button.btn.settings(data-toggle='coco-modal', data-target='core/AuthModal', data-i18n="[title]play.settings")
// Don't show these things, they are bad and take us out of the game. Just wait until the new ones work.
//else
// a.btn.achievements(href="/user/#{me.getSlugOrID()}/stats", data-i18n="[title]play.achievements")
// a.btn.account(href="/user/#{me.getSlugOrID()}", data-i18n="[title]play.account")
// a.btn.settings(href='/account', data-i18n="[title]play.settings")
.user-status.header-font
.user-status-line
span.gem.gem-30
span#gems-count.spr= me.gems()
span.player-level-icon
span.player-level.spr= me.level()
span.player-hero-icon
if me.get('anonymous')
span.player-name.spr(data-i18n="play.anonymous") Anonymous Player
button.btn.btn-illustrated.login-button.btn-warning(data-i18n="login.log_in")
button.btn.btn-illustrated.signup-button.btn-danger(data-i18n="signup.sign_up")
else
span.player-name.spr= me.get('name')
button#logout-button.btn.btn-illustrated.btn-warning(data-i18n="login.log_out") Log Out
if me.isPremium()
button.btn.btn-illustrated.btn-primary(data-i18n="nav.contact", data-toggle="coco-modal", data-target="core/ContactModal") Contact
button.btn.btn-lg.btn-inverse#volume-button(title="Adjust volume")
.glyphicon.glyphicon-volume-off
.glyphicon.glyphicon-volume-down
.glyphicon.glyphicon-volume-up
//h1#campaign-status
// if mapType == 'dungeon'
// span.spr(data-i18n="play.campaign_dungeon")
// else if mapType == 'forest'
// span.spr(data-i18n="play.campaign_forest")
// | -
// if requiresSubscription
// span.spl(data-i18n="play.subscription_required")
// else if mapType == 'dungeon'
// span.spl(data-i18n="play.free")
// else
// span.spl(data-i18n="play.subscribed")

View file

@ -8,7 +8,7 @@
- var seenNext = nextLevel; - var seenNext = nextLevel;
each level in campaign.levels each level in campaign.levels
if !level.hidden if !level.hidden
- var next = level.id == nextLevel || (!seenNext && levelStatusMap[level.id] != "complete" && !level.locked && !level.disabled); - var next = level.id == nextLevel || (!seenNext && levelStatusMap[level.id] != "complete" && !level.locked && !level.disabled && !editorMode);
- seenNext = seenNext || next; - seenNext = seenNext || next;
div(style="left: #{level.x}%; bottom: #{level.y}%; background-color: #{level.color}", class="level" + (next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + levelStatusMap[level.id] || "", data-level-id=level.id, title=level.name + (level.disabled ? ' (Coming Soon to Adventurers)' : '')) div(style="left: #{level.x}%; bottom: #{level.y}%; background-color: #{level.color}", class="level" + (next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + levelStatusMap[level.id] || "", data-level-id=level.id, title=level.name + (level.disabled ? ' (Coming Soon to Adventurers)' : ''))
if level.unlocksHero && !level.unlockedHero if level.unlocksHero && !level.unlockedHero

View file

@ -0,0 +1,944 @@
RootView = require 'views/core/RootView'
Campaign = require 'models/Campaign'
Level = require 'models/Level'
Achievement = require 'models/Achievement'
ThangType = require 'models/ThangType'
CampaignView = require 'views/play/CampaignView'
CocoCollection = require 'collections/CocoCollection'
treemaExt = require 'core/treema-ext'
utils = require 'core/utils'
SaveCampaignModal = require './SaveCampaignModal'
RelatedAchievementsCollection = require 'collections/RelatedAchievementsCollection'
CampaignLevelView = require './CampaignLevelView'
achievementProject = ['related', 'rewards', 'name', 'slug']
thangTypeProject = ['name', 'original', 'slug']
module.exports = class CampaignEditorView extends RootView
id: "campaign-editor-view"
template: require 'templates/editor/campaign/campaign-editor-view'
className: 'editor'
events:
'click #save-button': 'onClickSaveButton'
constructor: (options, @campaignHandle) ->
super(options)
# MIGRATION CODE
# for level in levels
# _.extend level, options[level.id]
# level.slug = level.id
# delete level.id
# delete level.nextLevels
# level.position = { x: level.x, y: level.y }
# delete level.x
# delete level.y
# if level.unlocksHero
# level.unlocks = [{
# original: level.unlocksHero.originalID
# type: 'hero'
# }]
# delete level.unlocksHero
# campaign.levels[level.original] = level
# @campaign = new Campaign(campaign)
#------------------------------------------------
@campaign = new Campaign({_id:@campaignHandle})
#--------------- temporary migration to change thang type slugs to originals
#- should keep around though for loading the names of items and heroes that are referenced
#- just load names instead of slugs, though
@sluggyThangs = new Backbone.Collection()
@listenToOnce @campaign, 'sync', ->
slugs = []
for level in _.values(@campaign.get('levels'))
slugs = slugs.concat(_.values(level.requiredGear)) if level.requiredGear
slugs = slugs.concat(_.values(level.restrictedGear)) if level.restrictedGear
slugs = slugs.concat(level.allowedHeroes) if level.allowedHeroes
slugs = _.uniq _.flatten slugs
for slug in slugs
thangType = new ThangType()
thangType.setProjection(thangTypeProject)
if utils.isID slug
thangType.setURL("/db/thang.type/#{slug}/version")
else
thangType.setURL("/db/thang.type/#{slug}")
@supermodel.loadModel(thangType, 'thang')
@sluggyThangs.add(thangType)
#---------------
@supermodel.loadModel(@campaign, 'campaign')
@levels = new CocoCollection([], {
model: Level
url: "/db/campaign/#{@campaignHandle}/levels"
project: Campaign.denormalizedLevelProperties
})
@supermodel.loadCollection(@levels, 'levels')
@achievements = new CocoCollection([], {
model: Achievement
url: "/db/campaign/#{@campaignHandle}/achievements"
project: achievementProject
})
@supermodel.loadCollection(@achievements, 'achievements')
@toSave = new Backbone.Collection()
@listenToOnce @campaign, 'sync', @onFundamentalLoaded
@listenToOnce @levels, 'sync', @onFundamentalLoaded
@listenToOnce @achievements, 'sync', @onFundamentalLoaded
onFundamentalLoaded: ->
# load any levels which
return unless @campaign.loaded and @levels.loaded and @achievements.loaded
for level in _.values(@campaign.get('levels'))
model = @levels.findWhere(original: level.original)
if not model
model = new Level({})
model.setProjection Campaign.denormalizedLevelProperties
model.setURL("/db/level/#{level.original}/version")
@levels.add @supermodel.loadModel(model, 'level').model
achievements = new RelatedAchievementsCollection level.original
achievements.setProjection achievementProject
@supermodel.loadCollection achievements, 'achievements'
@listenToOnce achievements, 'sync', ->
@achievements.add(achievements.models)
onLoaded: ->
@toSave.add @campaign if @campaign.hasLocalChanges()
campaignLevels = $.extend({}, @campaign.get('levels'))
for level in @levels.models
levelOriginal = level.get('original')
campaignLevel = campaignLevels[levelOriginal]
continue if not campaignLevel
#--------------- temporary migrations
if campaignLevel.restrictedGear
for slot, value of campaignLevel.restrictedGear
if _.isString(value)
campaignLevel.restrictedGear[slot] = [value]
#
if campaignLevel.requiredGear
for slot, value of campaignLevel.requiredGear
if _.isString(value)
campaignLevel.requiredGear[slot] = [value]
#
if campaignLevel.requiredGear
for gear in _.values(campaignLevel.requiredGear)
for slug, index in gear
thang = @sluggyThangs.findWhere({slug: slug})
continue unless thang
gear[index] = thang.get('original')
#
if campaignLevel.restrictedGear
for gear in _.values(campaignLevel.restrictedGear)
for slug, index in gear
thang = @sluggyThangs.findWhere({slug: slug})
continue unless thang
gear[index] = thang.get('original')
#
if campaignLevel.allowedHeroes
for slug, index in campaignLevel.allowedHeroes
thang = @sluggyThangs.findWhere({slug: slug})
continue unless thang
level.allowedHeroes[index] = thang.get('original')
#---------------
$.extend campaignLevel, _.omit(level.attributes, '_id')
achievements = @achievements.where {'related': levelOriginal}
rewards = []
for achievement in achievements
for rewardType, rewardArray of achievement.get('rewards')
for reward in rewardArray
rewardObject = { achievement: achievement.id }
if rewardType is 'heroes'
rewardObject.hero = reward
thangType = new ThangType({}, {project: thangTypeProject})
thangType.setURL("/db/thang.type/#{reward}/version")
@supermodel.loadModel(thangType, 'thang')
if rewardType is 'levels'
rewardObject.level = reward
if not @levels.findWhere({original: reward})
level = new Level({}, {project: Campaign.denormalizedLevelProperties})
level.setURL("/db/level/#{reward}/version")
@supermodel.loadModel(level, 'level')
if rewardType is 'items'
rewardObject.item = reward
thangType = new ThangType({}, {project: thangTypeProject})
thangType.setURL("/db/thang.type/#{reward}/version")
@supermodel.loadModel(thangType, 'thang')
rewards.push rewardObject
campaignLevel.rewards = rewards
delete campaignLevel.unlocks
campaignLevels[levelOriginal] = campaignLevel
@campaign.set('levels', campaignLevels)
for level in _.values campaignLevels
model = @levels.findWhere {original: level.original}
model.set key, level[key] for key in Campaign.denormalizedLevelProperties
# @toSave.add model if model.hasLocalChanges()
# @updateRewardsForLevel model, level.rewards
super()
getRenderData: ->
c = super()
c.campaign = @campaign
c
onClickSaveButton: ->
@toSave.set @toSave.filter (m) -> m.hasLocalChanges()
@openModalView new SaveCampaignModal({}, @toSave)
afterRender: ->
super()
treemaOptions =
schema: Campaign.schema
data: $.extend({}, @campaign.attributes)
callbacks:
change: @onTreemaChanged
select: @onTreemaSelectionChanged
dblclick: @onTreemaDoubleClicked
nodeClasses:
levels: LevelsNode
level: LevelNode
campaigns: CampaignsNode
campaign: CampaignNode
achievement: AchievementNode
supermodel: @supermodel
@treema = @$el.find('#campaign-treema').treema treemaOptions
@treema.build()
@treema.open()
@treema.childrenTreemas.levels?.open()
@campaignView = new CampaignView({editorMode: true, supermodel: @supermodel}, @campaignHandle)
@campaignView.highlightElement = _.noop # make it stop
@listenTo @campaignView, 'level-moved', @onCampaignLevelMoved
@listenTo @campaignView, 'adjacent-campaign-moved', @onAdjacentCampaignMoved
@listenTo @campaignView, 'level-clicked', @onCampaignLevelClicked
@insertSubView @campaignView
onTreemaChanged: (e, nodes) =>
for node in nodes
path = node.getPath()
if _.string.startsWith path, '/levels/'
parts = path.split('/')
original = parts[2]
level = @supermodel.getModelByOriginal Level, original
campaignLevel = @treema.get "/levels/#{original}"
@updateRewardsForLevel level, campaignLevel.rewards
level.set key, campaignLevel[key] for key in Campaign.denormalizedLevelProperties
@toSave.add level if level.hasLocalChanges()
@toSave.add @campaign
@campaign.set key, value for key, value of @treema.data
@campaignView.setCampaign(@campaign)
onTreemaDoubleClicked: (e, node) =>
path = node.getPath()
return unless _.string.startsWith path, '/levels/'
original = path.split('/')[2]
level = @supermodel.getModelByOriginal Level, original
@insertSubView new CampaignLevelView({}, level)
onCampaignLevelMoved: (e) ->
path = "levels/#{e.levelOriginal}/position"
@treema.set path, e.position
onAdjacentCampaignMoved: (e) ->
path = "adjacentCampaigns/#{e.campaignID}/position"
@treema.set path, e.position
onCampaignLevelClicked: (levelOriginal) ->
return unless levelTreema = @treema.childrenTreemas?.levels?.childrenTreemas?[levelOriginal]
levelTreema.select()
# levelTreema.open()
updateRewardsForLevel: (level, rewards) ->
achievements = @supermodel.getModels(Achievement)
achievements = (a for a in achievements when a.get('related') is level.get('original'))
for achievement in achievements
rewardSubset = (r for r in rewards when r.achievement is achievement.id)
oldRewards = achievement.get 'rewards'
newRewards = {}
heroes = _.compact((r.hero for r in rewardSubset))
newRewards.heroes = heroes if heroes.length
items = _.compact((r.item for r in rewardSubset))
newRewards.items = items if items.length
levels = _.compact((r.level for r in rewardSubset))
newRewards.levels = levels if levels.length
newRewards.gems = oldRewards.gems if oldRewards.gems
achievement.set 'rewards', newRewards
if achievement.hasLocalChanges()
@toSave.add achievement
class LevelsNode extends TreemaObjectNode
valueClass: 'treema-levels'
@levels: {}
buildValueForDisplay: (valEl, data) ->
@buildValueForDisplaySimply valEl, ''+_.size(data)
childPropertiesAvailable: -> @childSource
childSource: (req, res) =>
s = new Backbone.Collection([], {model:Level})
s.url = '/db/level'
s.fetch({data: {term:req.term, project: Campaign.denormalizedLevelProperties.join(',')}})
s.once 'sync', (collection) =>
for level in collection.models
LevelsNode.levels[level.get('original')] = level
@settings.supermodel.registerModel level
mapped = ({label: r.get('name'), value: r.get('original')} for r in collection.models)
res(mapped)
class LevelNode extends TreemaObjectNode
valueClass: 'treema-level'
buildValueForDisplay: (valEl, data) ->
@buildValueForDisplaySimply valEl, data.name
populateData: ->
return if @data.name?
data = _.pick LevelsNode.levels[@keyForParent].attributes, Campaign.denormalizedLevelProperties
_.extend @data, data
class CampaignsNode extends TreemaObjectNode
valueClass: 'treema-campaigns'
@campaigns: {}
buildValueForDisplay: (valEl, data) ->
@buildValueForDisplaySimply valEl, ''+_.size(data)
childPropertiesAvailable: -> @childSource
childSource: (req, res) =>
s = new Backbone.Collection([], {model:Campaign})
s.url = '/db/campaign'
s.fetch({data: {term:req.term, project: campaign.denormalizedCampaignProperties}})
s.once 'sync', (collection) ->
CampaignsNode.campaigns[campaign.id] = campaign for campaign in collection.models
mapped = ({label: r.get('name'), value: r.id} for r in collection.models)
res(mapped)
class CampaignNode extends TreemaObjectNode
valueClass: 'treema-campaign'
buildValueForDisplay: (valEl, data) ->
@buildValueForDisplaySimply valEl, data.name
populateData: ->
return if @data.name?
data = _.pick CampaignsNode.campaigns[@keyForParent].attributes, Campaign.denormalizedCampaignProperties
_.extend @data, data
class AchievementNode extends treemaExt.IDReferenceNode
buildSearchURL: (term) -> "#{@url}?term=#{term}&project=#{achievementProject.join(',')}"
#campaign = {
# name: 'Dungeon'
# levels: {}
#}
#
#
#levels = [
# {
# name: 'Dungeons of Kithgard'
# type: 'hero'
# id: 'dungeons-of-kithgard'
# original: '5411cb3769152f1707be029c'
# description: 'Grab the gem, but touch nothing else. Start here.'
# x: 14
# y: 15.5
# nextLevels:
# continue: 'gems-in-the-deep'
# }
# {
# name: 'Gems in the Deep'
# type: 'hero'
# id: 'gems-in-the-deep'
# original: '54173c90844506ae0195a0b4'
# description: 'Quickly collect the gems; you will need them.'
# x: 29
# y: 12
# nextLevels:
# continue: 'shadow-guard'
# }
# {
# name: 'Shadow Guard'
# type: 'hero'
# id: 'shadow-guard'
# original: '54174347844506ae0195a0b8'
# description: 'Evade the Kithgard minion.'
# x: 40.54
# y: 11.03
# nextLevels:
# continue: 'forgetful-gemsmith'
# }
# {
# name: 'Kounter Kithwise'
# type: 'hero'
# id: 'kounter-kithwise'
# original: '54527a6257e83800009730c7'
# description: 'Practice your evasion skills with more guards.'
# x: 35.37
# y: 20.61
# nextLevels:
# continue: 'crawlways-of-kithgard'
# practice: true
# requiresSubscription: true
# }
# {
# name: 'Crawlways of Kithgard'
# type: 'hero'
# id: 'crawlways-of-kithgard'
# original: '545287ef57e83800009730d5'
# description: 'Dart in and grab the gemat the right moment.'
# x: 36.48
# y: 29.03
# nextLevels:
# continue: 'forgetful-gemsmith'
# practice: true
# requiresSubscription: true
# }
# {
# name: 'Forgetful Gemsmith'
# type: 'hero'
# id: 'forgetful-gemsmith'
# original: '544a98f62d002f0000fe331a'
# description: 'Grab even more gems as you practice moving.'
# x: 54.98
# y: 10.53
# nextLevels:
# continue: 'true-names'
# }
# {
# name: 'True Names'
# type: 'hero'
# id: 'true-names'
# original: '541875da4c16460000ab990f'
# description: 'Learn an enemy\'s true name to defeat it.'
# x: 68.44
# y: 10.70
# nextLevels:
# continue: 'the-raised-sword'
# unlocksHero: {
# img: '/file/db/thang.type/53e12be0d042f23505c3023b/portrait.png'
# originalID: '53e12be0d042f23505c3023b'
# }
# }
# {
# name: 'Favorable Odds'
# type: 'hero'
# id: 'favorable-odds'
# original: '5452972f57e83800009730de'
# description: 'Test out your battle skills by defeating more munchkins.'
# x: 88.25
# y: 14.92
# nextLevels:
# continue: 'the-raised-sword'
# practice: true
# requiresSubscription: true
# }
# {
# name: 'The Raised Sword'
# type: 'hero'
# id: 'the-raised-sword'
# original: '5418aec24c16460000ab9aa6'
# description: 'Learn to equip yourself for combat.'
# x: 81.51
# y: 17.92
# nextLevels:
# continue: 'haunted-kithmaze'
# }
# {
# name: 'Haunted Kithmaze'
# type: 'hero'
# id: 'haunted-kithmaze'
# original: '545a5914d820eb0000f6dc0a'
# description: 'The builders of Kithgard constructed many mazes to confuse travelers.'
# x: 78
# y: 29
# nextLevels:
# continue: 'the-second-kithmaze'
# }
# {
# name: 'Riddling Kithmaze'
# type: 'hero'
# id: 'riddling-kithmaze'
# original: '5418b9d64c16460000ab9ab4'
# description: 'If at first you go astray, change your loop to find the way.'
# x: 69.97
# y: 28.03
# nextLevels:
# continue: 'descending-further'
# practice: true
# requiresSubscription: true
# }
# {
# name: 'Descending Further'
# type: 'hero'
# id: 'descending-further'
# original: '5452a84d57e83800009730e4'
# description: 'Another day, another maze.'
# x: 61.68
# y: 22.80
# nextLevels:
# continue: 'the-second-kithmaze'
# practice: true
# requiresSubscription: true
# }
# {
# name: 'The Second Kithmaze'
# type: 'hero'
# id: 'the-second-kithmaze'
# original: '5418cf256bae62f707c7e1c3'
# description: 'Many have tried, few have found their way through this maze.'
# x: 54.49
# y: 26.49
# nextLevels:
# continue: 'dread-door'
# }
# {
# name: 'Dread Door'
# type: 'hero'
# id: 'dread-door'
# original: '5418d40f4c16460000ab9ac2'
# description: 'Behind a dread door lies a chest full of riches.'
# x: 60.52
# y: 33.70
# nextLevels:
# continue: 'known-enemy'
# }
# {
# name: 'Known Enemy'
# type: 'hero'
# id: 'known-enemy'
# original: '5452adea57e83800009730ee'
# description: 'Begin to use variables in your battles.'
# x: 67
# y: 39
# nextLevels:
# continue: 'master-of-names'
# }
# {
# name: 'Master of Names'
# type: 'hero'
# id: 'master-of-names'
# original: '5452c3ce57e83800009730f7'
# description: 'Use your glasses to defend yourself from the Kithmen.'
# x: 75
# y: 46
# nextLevels:
# continue: 'lowly-kithmen'
# }
# {
# name: 'Lowly Kithmen'
# type: 'hero'
# id: 'lowly-kithmen'
# original: '541b24511ccc8eaae19f3c1f'
# description: 'Now that you can see them, they\'re everywhere!'
# x: 85
# y: 40
# nextLevels:
# continue: 'closing-the-distance'
# }
# {
# name: 'Closing the Distance'
# type: 'hero'
# id: 'closing-the-distance'
# original: '541b288e1ccc8eaae19f3c25'
# description: 'Kithmen are not the only ones to stand in your way.'
# x: 93
# y: 47
# nextLevels:
# continue: 'the-final-kithmaze'
# }
# {
# name: 'Tactical Strike'
# type: 'hero'
# id: 'tactical-strike'
# original: '5452cfa706a59e000067e4f5'
# description: 'They\'re, uh, coming right for us! Sneak up behind them.'
# x: 83.23
# y: 52.73
# nextLevels:
# continue: 'the-final-kithmaze'
# practice: true
# requiresSubscription: true
# }
# {
# name: 'The Final Kithmaze'
# type: 'hero'
# id: 'the-final-kithmaze'
# original: '541b434e1ccc8eaae19f3c33'
# description: 'To escape you must find your way through an Elder Kithman\'s maze.'
# x: 86.95
# y: 64.70
# nextLevels:
# continue: 'kithgard-gates'
# }
# {
# name: 'The Gauntlet'
# type: 'hero'
# id: 'the-gauntlet'
# original: '5452d8b906a59e000067e4fa'
# description: 'Rush for the stairs, battling foes at every turn.'
# x: 76.50
# y: 72.69
# nextLevels:
# continue: 'kithgard-gates'
# practice: true
# requiresSubscription: true
# }
# {
# name: 'Kithgard Gates'
# type: 'hero'
# id: 'kithgard-gates'
# original: '541c9a30c6362edfb0f34479'
# description: 'Escape the Kithgard dungeons and don\'t let the guardians get you.'
# x: 89
# y: 82
# nextLevels:
# continue: 'defense-of-plainswood'
# }
# {
# name: 'Cavern Survival'
# type: 'hero-ladder'
# id: 'cavern-survival'
# original: '544437e0645c0c0000c3291d'
# description: 'Stay alive longer than your opponent amidst hordes of ogres!'
# x: 17.54
# y: 78.39
# }
#]
#
#options =
# 'dungeons-of-kithgard':
# disableSpaces: true
# hidesSubmitUntilRun: true
# hidesPlayButton: true
# hidesRunShortcut: true
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots'}
# restrictedGear: {feet: 'leather-boots'}
# requiredCode: ['moveRight']
# 'gems-in-the-deep':
# disableSpaces: true
# hidesSubmitUntilRun: true
# hidesPlayButton: true
# hidesRunShortcut: true
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots'}
# restrictedGear: {feet: 'leather-boots'}
# 'shadow-guard':
# disableSpaces: true
# hidesSubmitUntilRun: true
# hidesPlayButton: true
# hidesRunShortcut: true
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots'}
# restrictedGear: {feet: 'leather-boots', 'right-hand': 'simple-sword'}
# 'kounter-kithwise':
# disableSpaces: true
# hidesPlayButton: true
# hidesRunShortcut: true
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots'}
# restrictedGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'}
# 'crawlways-of-kithgard':
# hidesPlayButton: true
# hidesRunShortcut: true
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots'}
# restrictedGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'}
# 'forgetful-gemsmith':
# disableSpaces: true
# hidesPlayButton: true
# hidesRunShortcut: true
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots'}
# restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'}
# 'true-names':
# disableSpaces: true
# hidesPlayButton: true
# hidesRunShortcut: true
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', waist: 'leather-belt'}
# restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'}
# requiredCode: ['Brak']
# 'favorable-odds':
# disableSpaces: true
# hidesPlayButton: true
# hidesRunShortcut: true
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword'}
# restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'}
# 'the-raised-sword':
# disableSpaces: true
# hidesPlayButton: true
# hidesRunShortcut: true
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate'}
# restrictedGear: {feet: 'leather-boots', 'programming-book': 'programmaticon-i'}
# 'the-first-kithmaze':
# hidesRunShortcut: true
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'}
# restrictedGear: {feet: 'leather-boots'}
# requiredCode: ['loop']
# 'haunted-kithmaze':
# hidesRunShortcut: true
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# moveRightLoopSnippet: true
# requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'}
# restrictedGear: {feet: 'leather-boots'}
# requiredCode: ['loop']
# 'descending-further':
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'}
# restrictedGear: {feet: 'leather-boots'}
# 'the-second-kithmaze':
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# moveRightLoopSnippet: true
# requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'}
# restrictedGear: {feet: 'leather-boots'}
# 'dread-door':
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'}
# restrictedGear: {feet: 'leather-boots'}
# 'known-enemy':
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', torso: 'tarnished-bronze-breastplate'}
# restrictedGear: {feet: 'leather-boots'}
# suspectCode: [{name: 'enemy-in-quotes', pattern: '[\'"]enemy'}] # '
# 'master-of-names':
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses', torso: 'tarnished-bronze-breastplate'}
# restrictedGear: {feet: 'leather-boots'}
# requiredCode: ['findNearestEnemy']
# suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}]
# 'lowly-kithmen':
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses', torso: 'tarnished-bronze-breastplate'}
# restrictedGear: {feet: 'leather-boots'}
# requiredCode: ['findNearestEnemy']
# suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}]
# 'closing-the-distance':
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', eyes: 'crude-glasses'}
# restrictedGear: {feet: 'leather-boots'}
# suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}]
# 'tactical-strike':
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', eyes: 'crude-glasses'}
# restrictedGear: {feet: 'leather-boots'}
# suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}]
# 'the-final-kithmaze':
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
# suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}]
# 'the-gauntlet':
# hidesHUD: true
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
# restrictedGear: {feet: 'leather-boots', 'right-hand': 'crude-builders-hammer'}
# suspectCode: [{name: 'lone-find-nearest-enemy', pattern: '^[ ]*(self|this|@)?[:.]?findNearestEnemy()'}]
# 'kithgard-gates':
# hidesSay: true
# hidesCodeToolbar: true
# hidesRealTimePlayback: true
# requiredGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', torso: 'tarnished-bronze-breastplate'}
# restrictedGear: {'right-hand': 'simple-sword'}
# 'defense-of-plainswood':
# hidesRealTimePlayback: true
# hidesCodeToolbar: true
# requiredGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer'}
# restrictedGear: {'right-hand': 'simple-sword'}
# 'winding-trail':
# hidesRealTimePlayback: true
# hidesCodeToolbar: true
# requiredGear: {feet: 'leather-boots', 'right-hand': 'crude-builders-hammer'}
# restrictedGear: {feet: 'simple-boots', 'right-hand': 'simple-sword'}
# 'patrol-buster':
# hidesRealTimePlayback: true
# hidesCodeToolbar: true
# requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'}
# restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'}
# 'endangered-burl':
# hidesRealTimePlayback: true
# hidesCodeToolbar: true
# requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'}
# restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'}
# 'village-guard':
# hidesCodeToolbar: true
# lockDefaultCode: true
# requiredGear: {feet: 'leather-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'}
# restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'}
# 'thornbush-farm':
# hidesCodeToolbar: true
# lockDefaultCode: true
# requiredGear: {feet: 'leather-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses'}
# restrictedGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'}
# requiredCode: ['topEnemy']
# 'back-to-back':
# hidesCodeToolbar: true
# requiredGear: {feet: 'leather-boots', torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'simple-sword', 'left-hand': 'wooden-shield'}
# restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'}
# 'ogre-encampment':
# requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'simple-sword', 'left-hand': 'wooden-shield'}
# restrictedGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i'}
# 'woodland-cleaver':
# requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'long-sword', 'left-hand': 'wooden-shield', wrists: 'sundial-wristwatch', feet: 'leather-boots'}
# restrictedGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i'}
# 'shield-rush':
# requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
# restrictedGear: {'left-hand': 'wooden-shield', 'programming-book': 'programmaticon-i'}
#
## Warrior branch
# 'peasant-protection':
# requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
# restrictedGear: {eyes: 'crude-glasses', 'programming-book': 'programmaticon-i'}
# 'munchkin-swarm':
# requiredGear: {torso: 'tarnished-bronze-breastplate', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
# restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
#
## Ranger branch
# 'munchkin-harvest':
# requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
# restrictedGear: {'programming-book': 'programmaticon-i'}
# allowedHeroes: ['captain', 'knight', 'samurai']
# 'swift-dagger':
# requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'crude-crossbow', 'left-hand': 'crude-dagger', wrists: 'sundial-wristwatch'}
# restrictedGear: {eyes: 'crude-glasses', 'programming-book': 'programmaticon-i'}
# allowedHeroes: ['ninja', 'trapper', 'forest-archer']
# 'shrapnel':
# requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'crude-crossbow', 'left-hand': 'weak-charge', wrists: 'sundial-wristwatch'}
# restrictedGear: {eyes: 'crude-glasses', 'left-hand': 'crude-dagger', 'programming-book': 'programmaticon-i'}
# allowedHeroes: ['ninja', 'trapper', 'forest-archer']
#
## Wizard branch
# 'arcane-ally':
# requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
# restrictedGear: {eyes: 'crude-glasses', 'programming-book': 'programmaticon-i'}
# allowedHeroes: ['captain', 'knight', 'samurai']
# 'touch-of-death':
# requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'enchanted-stick', 'left-hand': 'unholy-tome-i', wrists: 'sundial-wristwatch'}
# restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
# allowedHeroes: ['librarian', 'potion-master', 'sorcerer']
# 'bonemender':
# requiredGear: {waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'enchanted-stick', 'left-hand': 'book-of-life-i', wrists: 'sundial-wristwatch'}
# restrictedGear: {'left-hand': 'unholy-tome-i', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
# requiredCode: ['canCast']
# allowedHeroes: ['librarian', 'potion-master', 'sorcerer']
#
# 'coinucopia':
# requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags'}
# restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
# 'copper-meadows':
# requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses'}
# restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
# 'drop-the-flag':
# requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', 'right-hand': 'crude-builders-hammer'}
# restrictedGear: {'right-hand': 'long-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
# 'deadly-pursuit':
# requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', 'right-hand': 'crude-builders-hammer'}
# restrictedGear: {'right-hand': 'long-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
# 'rich-forager':
# requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', torso: 'tarnished-bronze-breastplate', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield'}
# restrictedGear: {'right-hand': 'crude-builders-hammer', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
# 'multiplayer-treasure-grove':
# requiredGear: {'programming-book': 'programmaticon-ii', feet: 'leather-boots', flag: 'basic-flags', eyes: 'wooden-glasses', torso: 'tarnished-bronze-breastplate'}
# restrictedGear: {'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
# 'siege-of-stonehold':
# requiredGear: {}
# restrictedGear: {}
#
## Desert
# 'the-dunes':
# requiredGear: {}
# restrictedGear: {}
# 'the-mighty-sand-yak':
# requiredGear: {}
# restrictedGear: {}
# 'oasis':
# requiredGear: {}
# restrictedGear: {}

View file

@ -0,0 +1,19 @@
CocoView = require 'views/core/CocoView'
module.exports = class CampaignLevelView extends CocoView
id: 'campaign-level-view'
template: require 'templates/editor/campaign/campaign-level-view'
events:
'click .close': 'onClickClose'
constructor: (options, @level) ->
super(options)
getRenderData: ->
c = super()
c.level = @level
c
onClickClose: ->
@$el.addClass('hidden')

View file

@ -0,0 +1,34 @@
ModalView = require 'views/core/ModalView'
template = require 'templates/editor/campaign/save-campaign-modal'
DeltaView = require 'views/editor/DeltaView'
module.exports = class SaveCampaignModal extends ModalView
id: 'save-campaign-modal'
template: template
plain: true
events:
'click #save-button': 'onClickSaveButton'
constructor: (options, @modelsToSave) ->
super(options)
getRenderData: ->
c = super()
c.modelsToSave = @modelsToSave
c
afterRender: ->
@$el.find('.delta-view').each((i, el) =>
$el = $(el)
model = @modelsToSave.find( id: $el.data('model-id'))
deltaView = new DeltaView({model: model})
@insertSubView(deltaView, $el)
)
super()
onClickSaveButton: ->
@showLoading()
modelsBeingSaved = (model.patch() for model in @modelsToSave.models)
modelsBeingSaved = modelsBeingSaved
$.when(_.compact(modelsBeingSaved)...).done(-> document.location.reload())

View file

@ -0,0 +1,384 @@
RootView = require 'views/core/RootView'
template = require 'templates/play/campaign-view'
LevelSession = require 'models/LevelSession'
EarnedAchievement = require 'models/EarnedAchievement'
CocoCollection = require 'collections/CocoCollection'
Campaign = require 'models/Campaign'
AudioPlayer = require 'lib/AudioPlayer'
LevelSetupManager = require 'lib/LevelSetupManager'
ThangType = require 'models/ThangType'
MusicPlayer = require 'lib/surface/MusicPlayer'
storage = require 'core/storage'
AuthModal = require 'views/core/AuthModal'
SubscribeModal = require 'views/core/SubscribeModal'
Level = require 'models/Level'
utils = require 'core/utils'
trackedHourOfCode = false
class LevelSessionsCollection extends CocoCollection
url: ''
model: LevelSession
constructor: (model) ->
super()
@url = "/db/user/#{me.id}/level.sessions?project=state.complete,levelID"
module.exports = class WorldMapView extends RootView
id: 'campaign-view'
template: template
subscriptions:
'subscribe-modal:subscribed': 'onSubscribed'
events:
'click .map-background': 'onClickMap'
'click .level a': 'onClickLevel'
'click .level-info-container .start-level': 'onClickStartLevel'
'mouseenter .level a': 'onMouseEnterLevel'
'mouseleave .level a': 'onMouseLeaveLevel'
'mousemove .map': 'onMouseMoveMap'
'click #volume-button': 'onToggleVolume'
constructor: (options, @terrain='dungeon') ->
if options and application.isIPAdApp # TODO: later only clear the SuperModel if it has received a memory warning (not in app store yet)
options.supermodel = null
super options
options ?= {}
@campaign = new Campaign({_id:@terrain})
@campaign = @supermodel.loadModel(@campaign, 'campaign').model
@editorMode = options.editorMode
@nextLevel = @getQueryVariable 'next'
@levelStatusMap = {}
@levelPlayCountMap = {}
@sessions = @supermodel.loadCollection(new LevelSessionsCollection(), 'your_sessions', null, 0).model
# Temporary attempt to make sure all earned rewards are accounted for. Figure out a better solution...
@earnedAchievements = new CocoCollection([], {url: '/db/earned_achievement', model:EarnedAchievement, project: ['earnedRewards']})
@listenToOnce @earnedAchievements, 'sync', ->
earned = me.get('earned')
for m in @earnedAchievements.models
continue unless loadedEarned = m.get('earnedRewards')
for group in ['heroes', 'levels', 'items']
continue unless loadedEarned[group]
for reward in loadedEarned[group]
if reward not in earned[group]
console.warn 'Filling in a gap for reward', group, reward
earned[group].push(reward)
@supermodel.loadCollection(@earnedAchievements, 'achievements')
@listenToOnce @sessions, 'sync', @onSessionsLoaded
@getLevelPlayCounts()
$(window).on 'resize', @onWindowResize
@playAmbientSound()
@probablyCachedMusic = storage.load("loaded-menu-music")
musicDelay = if @probablyCachedMusic then 1000 else 10000
@playMusicTimeout = _.delay (=> @playMusic() unless @destroyed), musicDelay
@hadEverChosenHero = me.get('heroConfig')?.thangType
@listenTo me, 'change:purchased', -> @renderSelectors('#gems-count')
@listenTo me, 'change:spent', -> @renderSelectors('#gems-count')
@listenTo me, 'change:heroConfig', -> @updateHero()
window.tracker?.trackEvent 'Loaded World Map', category: 'World Map', ['Google Analytics']
# If it's a new player who didn't appear to come from Hour of Code, we register her here without setting the hourOfCode property.
elapsed = (new Date() - new Date(me.get('dateCreated')))
if not trackedHourOfCode and not me.get('hourOfCode') and elapsed < 5 * 60 * 1000
$('body').append($('<img src="http://code.org/api/hour/begin_codecombat.png" style="visibility: hidden;">'))
trackedHourOfCode = true
@requiresSubscription = not me.isPremium()
destroy: ->
@setupManager?.destroy()
@$el.find('.ui-draggable').draggable 'destroy'
$(window).off 'resize', @onWindowResize
if ambientSound = @ambientSound
# Doesn't seem to work; stops immediately.
createjs.Tween.get(ambientSound).to({volume: 0.0}, 1500).call -> ambientSound.stop()
@musicPlayer?.destroy()
clearTimeout @playMusicTimeout
super()
getLevelPlayCounts: ->
return # TODO: Either use the campaign object instead of hardcoded data or get the data some other way
return unless me.isAdmin()
success = (levelPlayCounts) =>
return if @destroyed
for level in levelPlayCounts
@levelPlayCountMap[level._id] = playtime: level.playtime, sessions: level.sessions
@render() if @fullyRendered
levelIDs = []
for campaign in campaigns
for level in campaign.levels
levelIDs.push level.id
levelPlayCountsRequest = @supermodel.addRequestResource 'play_counts', {
url: '/db/level/-/play_counts'
data: {ids: levelIDs}
method: 'POST'
success: success
}, 0
levelPlayCountsRequest.load()
onLoaded: ->
return if @fullyRendered
@fullyRendered = true
@render()
@preloadTopHeroes() unless me.get('heroConfig')?.thangType
setCampaign: (@campaign) ->
@render()
onSubscribed: ->
@requiresSubscription = false
@render()
getRenderData: (context={}) ->
context = super(context)
context.campaign = @campaign
context.levels = _.values($.extend true, {}, @campaign.get('levels'))
for level in context.levels
level.position ?= { x: 10, y: 10 }
level.locked = not me.ownsLevel level.original
window.levelUnlocksNotWorking = true if level.locked and level.id is @nextLevel # Temporary
level.locked = false if window.levelUnlocksNotWorking # Temporary; also possible in HeroVictoryModal
level.locked = false if @levelStatusMap[level.id] in ['started', 'complete']
level.locked = false if me.get('slug') is 'nick'
level.locked = false if @editorMode
level.disabled = false if @levelStatusMap[level.id] in ['started', 'complete']
level.color = 'rgb(255, 80, 60)'
if level.requiresSubscription
level.color = 'rgb(80, 130, 200)'
if level.unlocksHero
level.unlockedHero = level.unlocksHero.originalID in (me.get('earned')?.heroes or [])
level.hidden = level.locked or level.disabled
# put lower levels in last, so in the world map they layer over one another properly.
context.campaign.levels = (_.sortBy context.campaign.levels, (l) -> l.position.y).reverse()
context.levelStatusMap = @levelStatusMap
context.levelPlayCountMap = @levelPlayCountMap
context.isIPadApp = application.isIPadApp
context.mapType = _.string.slugify @terrain
context.nextLevel = @nextLevel
context.forestIsAvailable = Level.levels['defense-of-plainswood'] in (me.get('earned')?.levels or [])
context.desertIsAvailable = Level.levels['the-mighty-sand-yak'] in (me.get('earned')?.levels or [])
context.requiresSubscription = @requiresSubscription
context.editorMode = @editorMode
context.adjacentCampaigns = _.filter _.values(_.cloneDeep(@campaign.get('adjacentCampaigns') or {})), (ac) ->
return false if ac.showIfUnlocked and ac.showIfUnlocked not in (me.get('unlocked')?.levels or [])
ac.name = utils.i18n ac, 'name'
ac.description = utils.i18n ac, 'description'
styles = []
styles.push "color: #{ac.color}" if ac.color
styles.push "transform: rotate(#{ac.rotation}deg)" if ac.rotation
ac.position ?= { x: 10, y: 10 }
styles.push "left: #{ac.position.x}%"
styles.push "top: #{ac.position.y}%"
ac.style = styles.join('; ')
return true
context
afterRender: ->
super()
@onWindowResize()
unless application.isIPadApp
_.defer => @$el?.find('.game-controls .btn').tooltip() # Have to defer or i18n doesn't take effect.
view = @
@$el.find('.level, .campaign-switch').tooltip().each ->
return unless me.isAdmin()
$(@).draggable().on 'dragstop', ->
bg = $('.map-background')
x = ($(@).offset().left - bg.offset().left + $(@).outerWidth() / 2) / bg.width()
y = 1 - ($(@).offset().top - bg.offset().top + $(@).outerHeight() / 2) / bg.height()
e = { position: { x: (100 * x), y: (100 * y) }, levelOriginal: $(@).data('level-id'), campaignID: $(@).data('campaign-id') }
view.trigger 'level-moved', e if e.levelOriginal
view.trigger 'adjacent-campaign-moved', e if e.campaignID
@$el.addClass _.string.slugify @terrain
@updateVolume()
@updateHero()
unless window.currentModal or not @fullyRendered
@highlightElement '.level.next', delay: 500, duration: 60000, rotation: 0, sides: ['top']
if levelID = @$el.find('.level.next').data('level-id')
@$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show() unless @editorMode
pos = @$el.find('.level.next').offset()
@adjustLevelInfoPosition pageX: pos.left, pageY: pos.top
@manuallyPositionedLevelInfoID = levelID
afterInsert: ->
super()
return unless @getQueryVariable 'signup'
return if me.get('email')
@endHighlight()
authModal = new AuthModal supermodel: @supermodel
authModal.mode = 'signup'
@openModalView authModal
onSessionsLoaded: (e) ->
return if @editorMode
for session in @sessions.models
@levelStatusMap[session.get('levelID')] = if session.get('state')?.complete then 'complete' else 'started'
if @nextLevel and @levelStatusMap[@nextLevel] is 'complete'
@nextLevel = null
@render()
onClickMap: (e) ->
@$levelInfo?.hide()
# Easy-ish way of figuring out coordinates for placing level dots.
x = e.offsetX / @$el.find('.map-background').width()
y = (1 - e.offsetY / @$el.find('.map-background').height())
console.log " x: #{(100 * x).toFixed(2)}\n y: #{(100 * y).toFixed(2)}\n"
onClickLevel: (e) ->
e.preventDefault()
e.stopPropagation()
@$levelInfo?.hide()
levelElement = $(e.target).parents('.level')
levelID = levelElement.data('level-id')
if @editorMode
return @trigger 'level-clicked', levelID
level = _.find _.values(@campaign.get('levels')), id: levelID
if application.isIPadApp
@$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show()
@adjustLevelInfoPosition e
@endHighlight()
else
if level.requiresSubscription and @requiresSubscription and not @levelStatusMap[level.id] and not level.adventurer
@openModalView new SubscribeModal()
window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'map level clicked', level: levelID
else if $(e.target).attr('disabled')
Backbone.Mediator.publish 'router:navigate', route: '/contribute/adventurer'
return
else if $(e.target).parent().hasClass 'locked'
return
else
@startLevel levelElement
window.tracker?.trackEvent 'Clicked Level', category: 'World Map', levelID: levelID, ['Google Analytics']
onClickStartLevel: (e) ->
levelElement = $(e.target).parents('.level-info-container')
@startLevel levelElement
window.tracker?.trackEvent 'Clicked Start Level', category: 'World Map', levelID: levelElement.data('level-id'), ['Google Analytics']
startLevel: (levelElement) ->
@setupManager?.destroy()
@setupManager = new LevelSetupManager supermodel: @supermodel, levelID: levelElement.data('level-id'), levelPath: levelElement.data('level-path'), levelName: levelElement.data('level-name'), hadEverChosenHero: @hadEverChosenHero, parent: @
@setupManager.open()
@$levelInfo?.hide()
onMouseEnterLevel: (e) ->
return if application.isIPadApp
return if @editorMode
levelID = $(e.target).parents('.level').data('level-id')
return if @manuallyPositionedLevelInfoID and levelID isnt @manuallyPositionedLevelInfoID
@$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show()
@adjustLevelInfoPosition e
@endHighlight()
@manuallyPositionedLevelInfoID = false
onMouseLeaveLevel: (e) ->
return if application.isIPadApp
levelID = $(e.target).parents('.level').data('level-id')
return if @manuallyPositionedLevelInfoID and levelID isnt @manuallyPositionedLevelInfoID
@$el.find(".level-info-container[data-level-id='#{levelID}']").hide()
@manuallyPositionedLevelInfoID = null
@$levelInfo = null
onMouseMoveMap: (e) ->
return if application.isIPadApp
@adjustLevelInfoPosition e unless @manuallyPositionedLevelInfoID
adjustLevelInfoPosition: (e) ->
return unless @$levelInfo
@$map ?= @$el.find('.map')
mapOffset = @$map.offset()
mapX = e.pageX - mapOffset.left
mapY = e.pageY - mapOffset.top
margin = 20
width = @$levelInfo.outerWidth()
@$levelInfo.css('left', Math.min(Math.max(margin, mapX - width / 2), @$map.width() - width - margin))
height = @$levelInfo.outerHeight()
top = mapY - @$levelInfo.outerHeight() - 60
if top < 20
top = mapY + 60
@$levelInfo.css('top', top)
onWindowResize: (e) =>
mapHeight = iPadHeight = 1536
mapWidth = {dungeon: 2350, forest: 2500, desert: 2350}[@terrain] or 2350
aspectRatio = mapWidth / mapHeight
pageWidth = @$el.width()
pageHeight = @$el.height()
widthRatio = pageWidth / mapWidth
heightRatio = pageHeight / mapHeight
# Make sure we can see the whole map, fading to background in one dimension.
if heightRatio <= widthRatio
# Left and right margin
resultingHeight = pageHeight
resultingWidth = resultingHeight * aspectRatio
else
# Top and bottom margin
resultingWidth = pageWidth
resultingHeight = resultingWidth / aspectRatio
resultingMarginX = (pageWidth - resultingWidth) / 2
resultingMarginY = (pageHeight - resultingHeight) / 2
@$el.find('.map').css(width: resultingWidth, height: resultingHeight, 'margin-left': resultingMarginX, 'margin-top': resultingMarginY)
playAmbientSound: ->
return if @ambientSound
return unless file = {dungeon: 'ambient-dungeon', forest: 'ambient-map-grass', desert: 'ambient-desert'}[@terrain]
src = "/file/interface/#{file}#{AudioPlayer.ext}"
unless AudioPlayer.getStatus(src)?.loaded
AudioPlayer.preloadSound src
Backbone.Mediator.subscribeOnce 'audio-player:loaded', @playAmbientSound, @
return
@ambientSound = createjs.Sound.play src, loop: -1, volume: 0.1
createjs.Tween.get(@ambientSound).to({volume: 0.5}, 1000)
playMusic: ->
@musicPlayer = new MusicPlayer()
musicFile = '/music/music-menu'
Backbone.Mediator.publish 'music-player:play-music', play: true, file: musicFile
storage.save("loaded-menu-music", true) unless @probablyCachedMusic
preloadTopHeroes: ->
for heroID in ['captain', 'knight']
url = "/db/thang.type/#{ThangType.heroes[heroID]}/version"
continue if @supermodel.getModel url
fullHero = new ThangType()
fullHero.setURL url
@supermodel.loadModel fullHero, 'thang'
updateVolume: (volume) ->
volume ?= me.get('volume') ? 1.0
classes = ['vol-off', 'vol-down', 'vol-up']
button = $('#volume-button', @$el)
button.toggleClass 'vol-off', volume <= 0.0
button.toggleClass 'vol-down', 0.0 < volume < 1.0
button.toggleClass 'vol-up', volume >= 1.0
createjs.Sound.setVolume(if volume is 1 then 0.6 else volume) # Quieter for now until individual sound FX controls work again.
if volume isnt me.get 'volume'
me.set 'volume', volume
me.patch()
onToggleVolume: (e) ->
button = $(e.target).closest('#volume-button')
classes = ['vol-off', 'vol-down', 'vol-up']
volumes = [0, 0.4, 1.0]
for oldClass, i in classes
if button.hasClass oldClass
newI = (i + 1) % classes.length
break
else if i is classes.length - 1 # no oldClass
newI = 2
@updateVolume volumes[newI]
updateHero: ->
return unless hero = me.get('heroConfig')?.thangType
for slug, original of ThangType.heroes when original is hero
@$el.find('.player-hero-icon').removeClass().addClass('player-hero-icon ' + slug)
return
console.error "WorldMapView hero update couldn't find hero slug for original:", hero

View file

@ -43,6 +43,8 @@ module.exports = class WorldMapView extends RootView
options.supermodel = null options.supermodel = null
@terrain ?= 'dungeon' # or 'forest', 'desert' @terrain ?= 'dungeon' # or 'forest', 'desert'
super options super options
options ?= {}
@editorMode = options.editorMode
@nextLevel = @getQueryVariable 'next' @nextLevel = @getQueryVariable 'next'
@levelStatusMap = {} @levelStatusMap = {}
@levelPlayCountMap = {} @levelPlayCountMap = {}
@ -97,6 +99,7 @@ module.exports = class WorldMapView extends RootView
super() super()
getLevelPlayCounts: -> getLevelPlayCounts: ->
return
return unless me.isAdmin() return unless me.isAdmin()
success = (levelPlayCounts) => success = (levelPlayCounts) =>
return if @destroyed return if @destroyed
@ -137,12 +140,12 @@ module.exports = class WorldMapView extends RootView
level.locked = false if window.levelUnlocksNotWorking # Temporary; also possible in HeroVictoryModal level.locked = false if window.levelUnlocksNotWorking # Temporary; also possible in HeroVictoryModal
level.locked = false if @levelStatusMap[level.id] in ['started', 'complete'] level.locked = false if @levelStatusMap[level.id] in ['started', 'complete']
level.locked = false if me.get('slug') is 'nick' level.locked = false if me.get('slug') is 'nick'
level.locked = false if @editorMode
level.disabled = false if @levelStatusMap[level.id] in ['started', 'complete'] level.disabled = false if @levelStatusMap[level.id] in ['started', 'complete']
level.color = 'rgb(255, 80, 60)' level.color = 'rgb(255, 80, 60)'
if level.requiresSubscription if level.requiresSubscription
level.color = 'rgb(80, 130, 200)' level.color = 'rgb(80, 130, 200)'
if level.unlocksHero if level.unlocksHero
level.color = 'rgb(0,0,0)'
level.unlockedHero = level.unlocksHero.originalID in (me.get('earned')?.heroes or []) level.unlockedHero = level.unlocksHero.originalID in (me.get('earned')?.heroes or [])
level.hidden = level.locked or level.disabled level.hidden = level.locked or level.disabled
@ -159,6 +162,7 @@ module.exports = class WorldMapView extends RootView
context.forestIsAvailable = @startedForestLevel or (Level.levels['defense-of-plainswood'] in (me.get('earned')?.levels or [])) context.forestIsAvailable = @startedForestLevel or (Level.levels['defense-of-plainswood'] in (me.get('earned')?.levels or []))
context.desertIsAvailable = @startedDesertLevel or (Level.levels['the-mighty-sand-yak'] in (me.get('earned')?.levels or [])) context.desertIsAvailable = @startedDesertLevel or (Level.levels['the-mighty-sand-yak'] in (me.get('earned')?.levels or []))
context.requiresSubscription = @requiresSubscription context.requiresSubscription = @requiresSubscription
context.editorMode = @editorMode
context context
afterRender: -> afterRender: ->
@ -179,7 +183,7 @@ module.exports = class WorldMapView extends RootView
unless window.currentModal or not @fullyRendered unless window.currentModal or not @fullyRendered
@highlightElement '.level.next', delay: 500, duration: 60000, rotation: 0, sides: ['top'] @highlightElement '.level.next', delay: 500, duration: 60000, rotation: 0, sides: ['top']
if levelID = @$el.find('.level.next').data('level-id') if levelID = @$el.find('.level.next').data('level-id')
@$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show() @$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show() unless @editorMode
pos = @$el.find('.level.next').offset() pos = @$el.find('.level.next').offset()
@adjustLevelInfoPosition pageX: pos.left, pageY: pos.top @adjustLevelInfoPosition pageX: pos.left, pageY: pos.top
@manuallyPositionedLevelInfoID = levelID @manuallyPositionedLevelInfoID = levelID
@ -194,6 +198,10 @@ module.exports = class WorldMapView extends RootView
@openModalView authModal @openModalView authModal
onSessionsLoaded: (e) -> onSessionsLoaded: (e) ->
if @editorMode
@startedForestLevel = true
@startedDesertLevel = true
return
forestLevels = (f.id for f in forest) forestLevels = (f.id for f in forest)
desertLevels = (f.id for f in desert) desertLevels = (f.id for f in desert)
for session in @sessions.models for session in @sessions.models
@ -249,6 +257,7 @@ module.exports = class WorldMapView extends RootView
onMouseEnterLevel: (e) -> onMouseEnterLevel: (e) ->
return if application.isIPadApp return if application.isIPadApp
return if @editorMode
levelID = $(e.target).parents('.level').data('level-id') levelID = $(e.target).parents('.level').data('level-id')
return if @manuallyPositionedLevelInfoID and levelID isnt @manuallyPositionedLevelInfoID return if @manuallyPositionedLevelInfoID and levelID isnt @manuallyPositionedLevelInfoID
@$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show() @$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show()
@ -287,8 +296,8 @@ module.exports = class WorldMapView extends RootView
mapHeight = iPadHeight = 1536 mapHeight = iPadHeight = 1536
mapWidth = {dungeon: 2350, forest: 2500, desert: 2350}[@terrain] or 2350 mapWidth = {dungeon: 2350, forest: 2500, desert: 2350}[@terrain] or 2350
aspectRatio = mapWidth / mapHeight aspectRatio = mapWidth / mapHeight
pageWidth = $(window).width() pageWidth = @$el.width()
pageHeight = $(window).height() pageHeight = @$el.height()
widthRatio = pageWidth / mapWidth widthRatio = pageWidth / mapWidth
heightRatio = pageHeight / mapHeight heightRatio = pageHeight / mapHeight
# Make sure we can see the whole map, fading to background in one dimension. # Make sure we can see the whole map, fading to background in one dimension.

View file

@ -0,0 +1,9 @@
mongoose = require 'mongoose'
plugins = require '../plugins/plugins'
CampaignSchema = new mongoose.Schema(body: String, {strict: false})
CampaignSchema.plugin(plugins.NamedPlugin)
CampaignSchema.plugin(plugins.TranslationCoveragePlugin)
module.exports = mongoose.model('campaign', CampaignSchema)

View file

@ -0,0 +1,70 @@
Campaign = require './Campaign'
Level = require '../levels/Level'
Achievement = require '../achievements/Achievement'
Handler = require '../commons/Handler'
async = require 'async'
mongoose = require 'mongoose'
CampaignHandler = class CampaignHandler extends Handler
modelClass: Campaign
editableProperties: [
'name'
'i18n'
'i18nCoverage'
'ambientSound'
'backgroundImage'
'backgroundColor'
'backgroundColorTransparent'
'adjacentCampaigns'
'levels'
]
jsonSchema: require '../../app/schemas/models/campaign.schema'
hasAccess: (req) ->
req.method is 'GET' or req.user?.isAdmin()
getByRelationship: (req, res, args...) ->
relationship = args[1]
if relationship in ['levels', 'achievements']
projection = {}
if req.query.project
projection[field] = 1 for field in req.query.project.split(',')
@getDocumentForIdOrSlug args[0], (err, campaign) =>
return @sendDatabaseError(res, err) if err
return @sendNotFoundError(res) unless campaign?
return @getRelatedLevels(req, res, campaign, projection) if relationship is 'levels'
return @getRelatedAchievements(req, res, campaign, projection) if relationship is 'achievements'
else
super(arguments...)
getRelatedLevels: (req, res, campaign, projection) ->
levels = campaign.get('levels') or []
f = (levelOriginal) ->
(callback) ->
query = { original: mongoose.Types.ObjectId(levelOriginal) }
sort = { 'version.major': -1, 'version.minor': -1 }
Level.findOne(query, projection).sort(sort).exec callback
fetches = (f(level.original) for level in _.values(levels))
async.parallel fetches, (err, levels) =>
return @sendDatabaseError(res, err) if err
return @sendSuccess(res, (level.toObject() for level in levels))
getRelatedAchievements: (req, res, campaign, projection) ->
levels = campaign.get('levels') or []
f = (levelOriginal) ->
(callback) ->
query = { related: levelOriginal }
Achievement.find(query, projection).exec callback
fetches = (f(level.original) for level in _.values(levels))
async.parallel fetches, (err, achievementses) =>
achievements = _.flatten(achievementses)
return @sendDatabaseError(res, err) if err
return @sendSuccess(res, (achievement.toObject() for achievement in achievements))
module.exports = new CampaignHandler()

View file

@ -328,7 +328,7 @@ module.exports = class Handler
put: (req, res, id) -> put: (req, res, id) ->
# Client expects PATCH behavior for PUTs # Client expects PATCH behavior for PUTs
# Real PATCHs return incorrect HTTP responses in some environments (e.g. Browserstack, schools) # Real PATCHs return incorrect HTTP responses in some environments (e.g. Browserstack, schools)
return @postNewVersion(req, res) if @modelClass.schema.uses_coco_versions return @sendForbiddenError(res) if @modelClass.schema.uses_coco_versions and not req.user.isAdmin()
return @sendBadInputError(res, 'No input.') if _.isEmpty(req.body) return @sendBadInputError(res, 'No input.') if _.isEmpty(req.body)
return @sendForbiddenError(res) unless @hasAccess(req) return @sendForbiddenError(res) unless @hasAccess(req)
@getDocumentForIdOrSlug req.body._id or id, (err, document) => @getDocumentForIdOrSlug req.body._id or id, (err, document) =>

View file

@ -3,6 +3,7 @@ module.exports.handlers =
# TODO: Disabling this until we know why our app servers CPU grows out of control. # TODO: Disabling this until we know why our app servers CPU grows out of control.
# 'analytics_users_active': 'analytics/analytics_users_active_handler' # 'analytics_users_active': 'analytics/analytics_users_active_handler'
'article': 'articles/article_handler' 'article': 'articles/article_handler'
'campaign': 'campaigns/campaign_handler'
'level': 'levels/level_handler' 'level': 'levels/level_handler'
'level_component': 'levels/components/level_component_handler' 'level_component': 'levels/components/level_component_handler'
'level_feedback': 'levels/feedbacks/level_feedback_handler' 'level_feedback': 'levels/feedbacks/level_feedback_handler'

View file

@ -31,6 +31,26 @@ LevelHandler = class LevelHandler extends Handler
'i18nCoverage' 'i18nCoverage'
'loadingTip' 'loadingTip'
'requiresSubscription' 'requiresSubscription'
'adventurer'
'practice'
'disableSpaces'
'hidesSubmitUntilRun'
'hidesPlayButton'
'hidesRunShortcut'
'hidesHUD'
'hidesSay'
'hidesCodeToolbar'
'hidesRealTimePlayback'
'backspaceThrottle'
'lockDefaultCode'
'moveRightLoopSnippet'
'realTimeSpeedFactor'
'autocompleteFontSizePx'
'requiredCode'
'suspectCode'
'requiredGear'
'restrictedGear'
'allowedHeroes'
'tasks' 'tasks'
] ]

View file

@ -26,6 +26,7 @@ GLOBAL.tv4 = require 'tv4' # required for TreemaUtils to work
models_path = [ models_path = [
'../../server/analytics/AnalyticsUsersActive' '../../server/analytics/AnalyticsUsersActive'
'../../server/articles/Article' '../../server/articles/Article'
'../../server/campaigns/Campaign'
'../../server/levels/Level' '../../server/levels/Level'
'../../server/levels/components/LevelComponent' '../../server/levels/components/LevelComponent'
'../../server/levels/systems/LevelSystem' '../../server/levels/systems/LevelSystem'

View file

@ -0,0 +1,77 @@
require '../common'
levels = [
{
name: 'Level 1'
description: 'This is the first level.'
disableSpaces: true
icon: 'somestringyoudontneed.png'
}
{
name: 'Level 2'
description: 'This is the second level.'
requiresSubscription: true
backspaceThrottle: true
}
]
achievement = {
name: 'Level 1 Complete'
}
campaign = {
name: 'Campaign'
levels: {}
}
levelURL = getURL('/db/level')
achievementURL = getURL('/db/achievement')
campaignURL = getURL('/db/campaign')
campaignSchema = require '../../../app/schemas/models/campaign.schema'
campaignLevelProperties = _.keys(campaignSchema.properties.levels.additionalProperties.properties)
describe '/db/campaign', ->
it 'prepares the db first', (done) ->
clearModels [Achievement, Campaign, Level, User], (err) ->
expect(err).toBeNull()
loginAdmin (admin) ->
levels[0].permissions = levels[1].permissions = [{target: admin._id, access: 'owner'}]
request.post {uri: levelURL, json: levels[0]}, (err, res, body) ->
expect(res.statusCode).toBe(200)
levels[0] = body
request.post {uri: levelURL, json: levels[1]}, (err, res, body) ->
expect(res.statusCode).toBe(200)
levels[1] = body
achievement.related = levels[0].original
achievement.rewards = { levels: [levels[1].original] }
request.post {uri: achievementURL, json: achievement}, (err, res, body) ->
achievement = body
done()
it 'can create campaigns', (done) ->
for level in levels.reverse()
campaign.levels[level.original] = _.pick level, campaignLevelProperties
request.post {uri: campaignURL, json: campaign}, (err, res, body) ->
expect(res.statusCode).toBe(200)
campaign = body
done()
describe '/db/campaign/.../levels', ->
it 'fetches the levels in a campaign', (done) ->
url = getURL("/db/campaign/#{campaign._id}/levels")
request.get {uri: url}, (err, res, body) ->
expect(res.statusCode).toBe(200)
body = JSON.parse(body)
expect(body.length).toBe(2)
expect(_.difference(['level-1', 'level-2'],(level.slug for level in body)).length).toBe(0)
done()
describe '/db/campaign/.../achievements', ->
it 'fetches the achievements in the levels in a campaign', (done) ->
url = getURL("/db/campaign/#{campaign._id}/achievements")
request.get {uri: url}, (err, res, body) ->
expect(res.statusCode).toBe(200)
body = JSON.parse(body)
expect(body.length).toBe(1)
done()