mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-04-26 14:03:28 -04:00
Merge branch 'feature/campaign-editor'
This commit is contained in:
commit
b4341ad46c
24 changed files with 2450 additions and 10 deletions
app
core
models
schemas/models
styles
templates
editor/campaign
play
views
server
campaigns
commons
levels
test/server
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
10
app/models/Campaign.coffee
Normal file
10
app/models/Campaign.coffee
Normal 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']
|
121
app/schemas/models/campaign.schema.coffee
Normal file
121
app/schemas/models/campaign.schema.coffee
Normal 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
|
|
@ -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'
|
||||
|
|
28
app/styles/editor/campaign/campaign-editor-view.sass
Normal file
28
app/styles/editor/campaign/campaign-editor-view.sass
Normal 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
|
495
app/styles/play/campaign-view.sass
Normal file
495
app/styles/play/campaign-view.sass
Normal 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
|
44
app/templates/editor/campaign/campaign-editor-view.jade
Normal file
44
app/templates/editor/campaign/campaign-editor-view.jade
Normal 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
|
4
app/templates/editor/campaign/campaign-level-view.jade
Normal file
4
app/templates/editor/campaign/campaign-level-view.jade
Normal file
|
@ -0,0 +1,4 @@
|
|||
.jumbotron
|
||||
.button.close(type="button", aria-hidden="true") ×
|
||||
h1= level.get('name')
|
||||
p= level.get('description')
|
21
app/templates/editor/campaign/save-campaign-modal.jade
Normal file
21
app/templates/editor/campaign/save-campaign-modal.jade
Normal 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
|
99
app/templates/play/campaign-view.jade
Normal file
99
app/templates/play/campaign-view.jade
Normal 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")
|
|
@ -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
|
||||
|
|
944
app/views/editor/campaign/CampaignEditorView.coffee
Normal file
944
app/views/editor/campaign/CampaignEditorView.coffee
Normal 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 gem–at 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: {}
|
19
app/views/editor/campaign/CampaignLevelView.coffee
Normal file
19
app/views/editor/campaign/CampaignLevelView.coffee
Normal 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')
|
34
app/views/editor/campaign/SaveCampaignModal.coffee
Normal file
34
app/views/editor/campaign/SaveCampaignModal.coffee
Normal 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())
|
384
app/views/play/CampaignView.coffee
Normal file
384
app/views/play/CampaignView.coffee
Normal 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
|
|
@ -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.
|
||||
|
|
9
server/campaigns/Campaign.coffee
Normal file
9
server/campaigns/Campaign.coffee
Normal 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)
|
70
server/campaigns/campaign_handler.coffee
Normal file
70
server/campaigns/campaign_handler.coffee
Normal 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()
|
|
@ -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) =>
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
]
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
77
test/server/functional/campaign_handler.spec.coffee
Normal file
77
test/server/functional/campaign_handler.spec.coffee
Normal 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()
|
||||
|
Loading…
Add table
Add a link
Reference in a new issue