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/thang': go('editor/thang/ThangTypeSearchView')
'editor/thang/:thangID': go('editor/thang/ThangTypeEditView')
'editor/campaign/:campaignID': go('editor/campaign/CampaignEditorView')
'employers': go('EmployersView')
@ -84,7 +85,7 @@ module.exports = class CocoRouter extends Backbone.Router
'multiplayer': go('MultiplayerView')
'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': go('ladder/MainLadderView')
'play/level/:levelID': go('play/level/PlayLevelView')

View file

@ -313,7 +313,7 @@ class InternationalizationNode extends TreemaNode.nodeMap.object
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>'
valueClass: 'treema-latest-version'
url: '/db/article'
@ -383,7 +383,11 @@ class LatestVersionReferenceNode extends TreemaNode
m = CocoModel.getReferencedModel(@getData(), @workingSchema)
data = @getData()
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
m = @settings.supermodel.getModelByOriginalAndMajorVersion(m.constructor, data.original, data.majorVersion)
if @instance and not m
@ -434,7 +438,7 @@ class LatestVersionReferenceNode extends TreemaNode
selected = @getSelectedResultEl()
return not selected.length
class LatestVersionOriginalReferenceNode extends LatestVersionReferenceNode
module.exports.LatestVersionOriginalReferenceNode = class LatestVersionOriginalReferenceNode extends LatestVersionReferenceNode
# Just for saving the original, not the major version.
saveChanges: ->
selected = @getSelectedResultEl()
@ -443,6 +447,15 @@ class LatestVersionOriginalReferenceNode extends LatestVersionReferenceNode
@data = fullValue.attributes.original
@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
# 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

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'}
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.extendSearchableProperties LevelSchema
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;
each level in campaign.levels
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;
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

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
@terrain ?= 'dungeon' # or 'forest', 'desert'
super options
options ?= {}
@editorMode = options.editorMode
@nextLevel = @getQueryVariable 'next'
@levelStatusMap = {}
@levelPlayCountMap = {}
@ -97,6 +99,7 @@ module.exports = class WorldMapView extends RootView
super()
getLevelPlayCounts: ->
return
return unless me.isAdmin()
success = (levelPlayCounts) =>
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 @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.color = 'rgb(0,0,0)'
level.unlockedHero = level.unlocksHero.originalID in (me.get('earned')?.heroes or [])
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.desertIsAvailable = @startedDesertLevel or (Level.levels['the-mighty-sand-yak'] in (me.get('earned')?.levels or []))
context.requiresSubscription = @requiresSubscription
context.editorMode = @editorMode
context
afterRender: ->
@ -179,7 +183,7 @@ module.exports = class WorldMapView extends RootView
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()
@$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
@ -194,6 +198,10 @@ module.exports = class WorldMapView extends RootView
@openModalView authModal
onSessionsLoaded: (e) ->
if @editorMode
@startedForestLevel = true
@startedDesertLevel = true
return
forestLevels = (f.id for f in forest)
desertLevels = (f.id for f in desert)
for session in @sessions.models
@ -249,6 +257,7 @@ module.exports = class WorldMapView extends RootView
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()
@ -287,8 +296,8 @@ module.exports = class WorldMapView extends RootView
mapHeight = iPadHeight = 1536
mapWidth = {dungeon: 2350, forest: 2500, desert: 2350}[@terrain] or 2350
aspectRatio = mapWidth / mapHeight
pageWidth = $(window).width()
pageHeight = $(window).height()
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.

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) ->
# Client expects PATCH behavior for PUTs
# 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 @sendForbiddenError(res) unless @hasAccess(req)
@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.
# 'analytics_users_active': 'analytics/analytics_users_active_handler'
'article': 'articles/article_handler'
'campaign': 'campaigns/campaign_handler'
'level': 'levels/level_handler'
'level_component': 'levels/components/level_component_handler'
'level_feedback': 'levels/feedbacks/level_feedback_handler'

View file

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

View file

@ -26,6 +26,7 @@ GLOBAL.tv4 = require 'tv4' # required for TreemaUtils to work
models_path = [
'../../server/analytics/AnalyticsUsersActive'
'../../server/articles/Article'
'../../server/campaigns/Campaign'
'../../server/levels/Level'
'../../server/levels/components/LevelComponent'
'../../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()