mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-28 01:55:38 -05:00
Merge branch 'master' into production
This commit is contained in:
commit
28d01738f7
22 changed files with 317 additions and 104 deletions
BIN
app/assets/images/pages/play/portal-background.png
Normal file
BIN
app/assets/images/pages/play/portal-background.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
BIN
app/assets/images/pages/play/portal-campaigns.png
Normal file
BIN
app/assets/images/pages/play/portal-campaigns.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 516 KiB |
|
@ -1,8 +1,10 @@
|
|||
// Helper for running tests through Karma.
|
||||
// Hooks into the test view logic for running tests.
|
||||
|
||||
|
||||
window.userObject = {_id:'1'}
|
||||
initialize = require('core/initialize');
|
||||
initialize.init();
|
||||
console.debug = function() {}; // Karma conf doesn't seem to work? Debug messages are still emitted when they shouldn't be.
|
||||
TestView = require('views/TestView');
|
||||
TestView.runTests();
|
||||
TestView.runTests();
|
||||
|
|
|
@ -43,6 +43,8 @@ init = ->
|
|||
handleNormalUrls()
|
||||
setUpMoment() # Set up i18n for moment
|
||||
|
||||
module.exports.init = init
|
||||
|
||||
handleNormalUrls = ->
|
||||
# http://artsy.github.com/blog/2012/06/25/replacing-hashbang-routes-with-pushstate/
|
||||
$(document).on 'click', "a[href^='/']", (event) ->
|
||||
|
|
|
@ -399,10 +399,6 @@
|
|||
thank_you: "Thank you for supporting CodeCombat."
|
||||
sorry_to_see_you_go: "Sorry to see you go! Please let us know what we could have done better."
|
||||
unsubscribe_feedback_placeholder: "O, what have we done?"
|
||||
levels: "Get more practice with bonus levels!"
|
||||
heroes: "More powerful heroes!"
|
||||
gems: "3500 bonus gems every month!"
|
||||
items: "Over 250 bonus items!"
|
||||
parent_button: "Ask your parent"
|
||||
parent_email_description: "We'll email them so they can buy you a CodeCombat subscription."
|
||||
parent_email_input_invalid: "Email address invalid."
|
||||
|
@ -416,7 +412,6 @@
|
|||
parents_blurb1: "With CodeCombat, your child learns by writing real code. They start by learning simple commands, and progress to more advanced topics."
|
||||
parents_blurb2: "For $9.99 USD/mo, they get new challenges every week and personal email support from professional programmers."
|
||||
parents_blurb3: "No Risk: 100% money back guarantee, easy 1-click unsubscribe."
|
||||
subscribe_button: "Subscribe"
|
||||
stripe_description: "Monthly Subscription"
|
||||
subscription_required_to_play: "You'll need a subscription to play this level."
|
||||
unlock_help_videos: "Subscribe to unlock all video tutorials."
|
||||
|
@ -1003,6 +998,7 @@
|
|||
play_counts: "Play Counts"
|
||||
feedback: "Feedback"
|
||||
payment_info: "Payment Info"
|
||||
campaigns: "Campaigns"
|
||||
|
||||
delta:
|
||||
added: "Added"
|
||||
|
|
|
@ -148,6 +148,14 @@ module.exports = class User extends CocoModel
|
|||
application.tracker.identify leaderboardsGroup: @leaderboardsGroup unless me.isAdmin()
|
||||
@leaderboardsGroup
|
||||
|
||||
getShowsPortal: ->
|
||||
return @showsPortal if @showsPortal?
|
||||
group = me.get('testGroupNumber')
|
||||
@showsPortal = if group < 128 then true else false
|
||||
@showsPortal = true if me.isAdmin()
|
||||
application.tracker.identify showsPortal: @showsPortal unless me.isAdmin()
|
||||
@showsPortal
|
||||
|
||||
getVideoTutorialStylesIndex: (numVideos=0)->
|
||||
# A/B Testing video tutorial styles
|
||||
# Not a constant number of videos available (e.g. could be 0, 1, 3, or 4 currently)
|
||||
|
|
|
@ -91,5 +91,6 @@ AchievementSchema.definitions = {}
|
|||
AchievementSchema.definitions['mongoQueryOperator'] = MongoQueryOperatorSchema
|
||||
AchievementSchema.definitions['mongoFindQuery'] = MongoFindQuerySchema
|
||||
c.extendTranslationCoverageProperties AchievementSchema
|
||||
c.extendPatchableProperties AchievementSchema
|
||||
|
||||
module.exports = AchievementSchema
|
||||
|
|
|
@ -124,5 +124,6 @@ _.extend CampaignSchema.properties, {
|
|||
|
||||
c.extendBasicProperties CampaignSchema, 'campaign'
|
||||
c.extendTranslationCoverageProperties CampaignSchema
|
||||
c.extendPatchableProperties CampaignSchema
|
||||
|
||||
module.exports = CampaignSchema
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
c = require './../schemas'
|
||||
|
||||
patchables = ['level', 'thang_type', 'level_system', 'level_component', 'article']
|
||||
patchables = ['level', 'thang_type', 'level_system', 'level_component', 'article', 'achievement', 'campaign']
|
||||
|
||||
PatchSchema = c.object({title: 'Patch', required: ['target', 'delta', 'commitMessage']}, {
|
||||
delta: {title: 'Delta', type: ['array', 'object']}
|
||||
|
|
|
@ -478,6 +478,80 @@ $gameControlMargin: 30px
|
|||
.particle-man
|
||||
z-index: 2
|
||||
|
||||
.portal
|
||||
position: relative
|
||||
width: 100%
|
||||
height: 100%
|
||||
background: transparent url(/images/pages/play/portal-background.png)
|
||||
display: flex
|
||||
align-items: center
|
||||
justify-content: center
|
||||
|
||||
.portals
|
||||
$campaignWidth: 317px
|
||||
$campaignHeight: 634px
|
||||
$campaignHoverScale: 1.2
|
||||
width: 6 * $campaignWidth
|
||||
height: $campaignHeight * $campaignHoverScale
|
||||
flex-wrap: nowrap
|
||||
display: flex
|
||||
overflow: hidden
|
||||
|
||||
.campaign
|
||||
width: $campaignWidth
|
||||
height: $campaignHeight
|
||||
margin-top: $campaignHeight * ($campaignHoverScale - 1) / 2
|
||||
background: transparent url(/images/pages/play/portal-campaigns.png) no-repeat 0 0
|
||||
display: inline-block
|
||||
flex-shrink: 0
|
||||
position: relative
|
||||
cursor: pointer
|
||||
// http://easings.net/#easeOutBack plus tweaked a bit: http://cubic-bezier.com/#.11,.67,.08,1.42
|
||||
@include transition(0.25s cubic-bezier(0.11, 0.67, 0.8, 1.42))
|
||||
|
||||
&:hover
|
||||
@include scale($campaignHoverScale)
|
||||
|
||||
&.silhouette
|
||||
@include filter(contrast(50%) brightness(65%))
|
||||
pointer-events: none
|
||||
|
||||
&.locked
|
||||
@include filter(contrast(80%) brightness(80%))
|
||||
pointer-events: none
|
||||
|
||||
&.forest
|
||||
background-position: (-1 * $campaignWidth) 0
|
||||
&.desert
|
||||
background-position: (-2 * $campaignWidth) 0
|
||||
&.mountain
|
||||
background-position: (-3 * $campaignWidth) 0
|
||||
&.ice
|
||||
background-position: (-4 * $campaignWidth) 0
|
||||
&.volcano
|
||||
background-position: (-5 * $campaignWidth) 0
|
||||
|
||||
.campaign-label
|
||||
position: absolute
|
||||
top: 55%
|
||||
width: 100%
|
||||
text-align: center
|
||||
|
||||
.campaign-name, .levels-completed, .campaign-locked
|
||||
margin: 0
|
||||
color: rgb(232, 217, 87)
|
||||
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
|
||||
z-index: 30
|
||||
pointer-events: none
|
||||
|
||||
.levels-completed
|
||||
font-size: 22px
|
||||
|
||||
.play-button
|
||||
margin-top: 30px
|
||||
min-width: 100px
|
||||
|
||||
|
||||
|
||||
body.ipad #campaign-view
|
||||
// iPad only supports up to Kithgard Gates for now.
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
span.glyphicon.glyphicon-ok
|
||||
#parents-info(data-i18n="subscribe.parents")
|
||||
|
||||
button.btn.btn-lg.btn-illustrated.purchase-button(data-i18n="subscribe.subscribe_button")
|
||||
button.btn.btn-lg.btn-illustrated.purchase-button(data-i18n="subscribe.subscribe_title")
|
||||
button.btn.btn-lg.btn-illustrated.parent-button(data-i18n="subscribe.parent_button")
|
||||
|
||||
if state === 'declined'
|
||||
|
|
|
@ -1,54 +1,77 @@
|
|||
.map
|
||||
.gradient.horizontal-gradient.top-gradient
|
||||
.gradient.vertical-gradient.right-gradient
|
||||
.gradient.horizontal-gradient.bottom-gradient
|
||||
.gradient.vertical-gradient.left-gradient
|
||||
.map-background(alt="", draggable="false")
|
||||
if campaign
|
||||
.map
|
||||
.gradient.horizontal-gradient.top-gradient
|
||||
.gradient.vertical-gradient.right-gradient
|
||||
.gradient.horizontal-gradient.bottom-gradient
|
||||
.gradient.vertical-gradient.left-gradient
|
||||
.map-background(alt="", draggable="false")
|
||||
|
||||
each level in levels
|
||||
if !level.hidden
|
||||
div(style="left: #{level.position.x}%; bottom: #{level.position.y}%; background-color: #{level.color}", class="level" + (level.next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + (levelStatusMap[level.slug] || ""), data-level-slug=level.slug, data-level-original=level.original, title=i18n(level, 'name') + (level.disabled ? ' (Coming Soon to Adventurers)' : ''))
|
||||
if level.unlocksHero && (!level.purchasedHero || editorMode)
|
||||
img.hero-portrait(src="/file/db/thang.type/#{level.unlocksHero}/portrait.png")
|
||||
a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.slug}", disabled=level.disabled, data-level-slug=level.slug, 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.slug] === 'complete'
|
||||
img.banner(src="/images/pages/play/level-banner-complete.png")
|
||||
if levelStatusMap[level.slug] === 'started'
|
||||
img.banner(src="/images/pages/play/level-banner-started.png")
|
||||
div(style="left: #{level.position.x}%; bottom: #{level.position.y}%", class="level-shadow" + (level.next ? " next" : "") + " " + (levelStatusMap[level.slug] || ""))
|
||||
.level-info-container(data-level-slug=level.slug, data-level-path=level.levelPath || 'level', data-level-name=level.name)
|
||||
- var playCount = levelPlayCountMap[level.slug]
|
||||
div(class="level-info " + (levelStatusMap[level.slug] || "") + (level.requiresSubscription ? " premium" : ""))
|
||||
.level-status
|
||||
h3= i18n(level, 'name') + (level.disabled ? " (Coming soon!)" : (level.locked ? " (Locked)" : ""))
|
||||
- var description = i18n(level, 'description') || level.description || ""
|
||||
.level-description!= marked(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.
|
||||
|
||||
if !level.disabled && !level.locked
|
||||
if playCount && playCount.sessions
|
||||
.play-counts.hidden
|
||||
span.spl.spr= playCount.sessions
|
||||
span(data-i18n="play.players") players
|
||||
span.spr , #{Math.round(playCount.playtime / 3600)}
|
||||
span(data-i18n="play.hours_played") hours played
|
||||
if levelStatusMap[level.slug] === 'complete'
|
||||
button.btn.btn-warning.btn.btn-lg.btn-illustrated.view-solutions(data-level-slug=level.slug)
|
||||
span(data-i18n="leaderboard.scores")
|
||||
button.btn.btn-success.btn.btn-lg.btn-illustrated.start-level(data-i18n="common.play") Play
|
||||
else if level.unlocksHero && !level.purchasedHero
|
||||
img.hero-portrait(src="/file/db/thang.type/#{level.unlocksHero}/portrait.png", style="left: #{level.position.x}%; bottom: #{level.position.y}%;")
|
||||
|
||||
for adjacentCampaign in adjacentCampaigns
|
||||
a(href=(editorMode ? "/editor/campaign/" : "/play/") + adjacentCampaign.slug)
|
||||
span.glyphicon.glyphicon-share-alt.campaign-switch(style=adjacentCampaign.style, title=adjacentCampaign.name, data-campaign-id=adjacentCampaign.id)
|
||||
each level in levels
|
||||
if !level.hidden
|
||||
div(style="left: #{level.position.x}%; bottom: #{level.position.y}%; background-color: #{level.color}", class="level" + (level.next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + (levelStatusMap[level.slug] || ""), data-level-slug=level.slug, data-level-original=level.original, title=i18n(level, 'name') + (level.disabled ? ' (Coming Soon to Adventurers)' : ''))
|
||||
if level.unlocksHero && (!level.purchasedHero || editorMode)
|
||||
img.hero-portrait(src="/file/db/thang.type/#{level.unlocksHero}/portrait.png")
|
||||
a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.slug}", disabled=level.disabled, data-level-slug=level.slug, 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.slug] === 'complete'
|
||||
img.banner(src="/images/pages/play/level-banner-complete.png")
|
||||
if levelStatusMap[level.slug] === 'started'
|
||||
img.banner(src="/images/pages/play/level-banner-started.png")
|
||||
div(style="left: #{level.position.x}%; bottom: #{level.position.y}%", class="level-shadow" + (level.next ? " next" : "") + " " + (levelStatusMap[level.slug] || ""))
|
||||
.level-info-container(data-level-slug=level.slug, data-level-path=level.levelPath || 'level', data-level-name=level.name)
|
||||
- var playCount = levelPlayCountMap[level.slug]
|
||||
div(class="level-info " + (levelStatusMap[level.slug] || "") + (level.requiresSubscription ? " premium" : ""))
|
||||
.level-status
|
||||
h3= i18n(level, 'name') + (level.disabled ? " (Coming soon!)" : (level.locked ? " (Locked)" : ""))
|
||||
- var description = i18n(level, 'description') || level.description || ""
|
||||
.level-description!= marked(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.
|
||||
|
||||
if !level.disabled && !level.locked
|
||||
if playCount && playCount.sessions
|
||||
.play-counts.hidden
|
||||
span.spl.spr= playCount.sessions
|
||||
span(data-i18n="play.players") players
|
||||
span.spr , #{Math.round(playCount.playtime / 3600)}
|
||||
span(data-i18n="play.hours_played") hours played
|
||||
if levelStatusMap[level.slug] === 'complete'
|
||||
button.btn.btn-warning.btn.btn-lg.btn-illustrated.view-solutions(data-level-slug=level.slug)
|
||||
span(data-i18n="leaderboard.scores")
|
||||
button.btn.btn-success.btn.btn-lg.btn-illustrated.start-level(data-i18n="common.play") Play
|
||||
else if level.unlocksHero && !level.purchasedHero
|
||||
img.hero-portrait(src="/file/db/thang.type/#{level.unlocksHero}/portrait.png", style="left: #{level.position.x}%; bottom: #{level.position.y}%;")
|
||||
|
||||
for adjacentCampaign in adjacentCampaigns
|
||||
a(href=(editorMode ? "/editor/campaign/" : "/play/") + adjacentCampaign.slug)
|
||||
span.glyphicon.glyphicon-share-alt.campaign-switch(style=adjacentCampaign.style, title=adjacentCampaign.name, data-campaign-id=adjacentCampaign.id)
|
||||
|
||||
else
|
||||
.portal
|
||||
.portals
|
||||
for campaignSlug in ['dungeon', 'forest', 'desert', 'mountain', 'ice', 'volcano']
|
||||
- var campaign = campaigns[campaignSlug];
|
||||
div(class="campaign #{campaignSlug}" + (campaign ? "" : " silhouette") + (campaign && campaign.locked ? " locked" : ""), data-campaign-slug=campaignSlug)
|
||||
.campaign-label
|
||||
h2.campaign-name
|
||||
if campaign
|
||||
span= i18n(campaign.attributes, 'fullName')
|
||||
else
|
||||
span ???
|
||||
if campaign && campaign.levelsTotal
|
||||
h3.levels-completed
|
||||
span= campaign.levelsCompleted
|
||||
| /
|
||||
span= campaign.levelsTotal
|
||||
if campaign && campaign.locked
|
||||
h3.campaign-locked(data-i18n="play.locked") Locked
|
||||
else if campaign
|
||||
btn(data-i18n="common.play").btn.btn-illustrated.btn-lg.btn-success.play-button
|
||||
|
||||
.game-controls.header-font
|
||||
button.btn.items(data-toggle='coco-modal', data-target='play/modal/PlayItemsModal', data-i18n="[title]play.items")
|
||||
|
@ -85,7 +108,7 @@ button.btn.btn-lg.btn-inverse#volume-button(data-i18n="[title]play.adjust_volume
|
|||
.glyphicon.glyphicon-volume-down
|
||||
.glyphicon.glyphicon-volume-up
|
||||
|
||||
if campaign.loaded
|
||||
if campaign && campaign.loaded
|
||||
h1#campaign-status
|
||||
.campaign-status-background
|
||||
.campaign-name
|
||||
|
|
|
@ -241,7 +241,7 @@ module.exports = class CocoView extends Backbone.View
|
|||
|
||||
showLoading: ($el=@$el) ->
|
||||
$el.find('>').addClass('hidden')
|
||||
$el.append loadingScreenTemplate()
|
||||
$el.append(loadingScreenTemplate()).i18n()
|
||||
@_lastLoading = $el
|
||||
|
||||
hideLoading: ->
|
||||
|
|
|
@ -140,9 +140,14 @@ module.exports = class I18NEditModelView extends RootView
|
|||
return _.isArray(delta.o) and delta.o.length is 1 and 'i18n' in delta.dataPath
|
||||
)
|
||||
|
||||
commitMessage = "Diplomat submission for lang #{@selectedLanguage}: #{flattened.length} change(s)."
|
||||
save = false if @savedBefore
|
||||
|
||||
if save
|
||||
modelToSave = @model.cloneNewMinorVersion()
|
||||
modelToSave.updateI18NCoverage() if modelToSave.get('i18nCoverage')
|
||||
if @modelClass.schema.properties.commitMessage
|
||||
modelToSave.set 'commitMessage', commitMessage
|
||||
|
||||
else
|
||||
modelToSave = new Patch()
|
||||
|
@ -151,17 +156,21 @@ module.exports = class I18NEditModelView extends RootView
|
|||
'collection': _.string.underscored @model.constructor.className
|
||||
'id': @model.id
|
||||
}
|
||||
|
||||
if @modelClass.schema.properties.commitMessage
|
||||
commitMessage = "Diplomat submission for lang #{@selectedLanguage}: #{flattened.length} change(s)."
|
||||
modelToSave.set 'commitMessage', commitMessage
|
||||
|
||||
errors = modelToSave.validate()
|
||||
button = $(e.target)
|
||||
button.attr('disabled', 'disabled')
|
||||
return button.text('Failed to Submit Changes') if errors
|
||||
res = modelToSave.save(null, {type: 'POST'}) # Override PUT so we can trigger postNewVersion logic
|
||||
type = 'PUT'
|
||||
if @modelClass.schema.properties.version or (not save)
|
||||
# Override PUT so we can trigger postNewVersion logic
|
||||
# or you're POSTing a Patch
|
||||
type = 'POST'
|
||||
res = modelToSave.save(null, {type: type})
|
||||
return button.text('Failed to Submit Changes') unless res
|
||||
button.text('Submitting...')
|
||||
res.error => button.text('Error Submitting Changes')
|
||||
res.success => button.text('Submit Changes')
|
||||
res.success =>
|
||||
@savedBefore = true
|
||||
button.text('Submit Changes')
|
||||
|
|
|
@ -27,6 +27,11 @@ class LevelSessionsCollection extends CocoCollection
|
|||
super()
|
||||
@url = "/db/user/#{me.id}/level.sessions?project=state.complete,levelID"
|
||||
|
||||
class CampaignsCollection extends CocoCollection
|
||||
url: '/db/campaign'
|
||||
model: Campaign
|
||||
project: ['name', 'fullName', 'i18n']
|
||||
|
||||
module.exports = class CampaignView extends RootView
|
||||
id: 'campaign-view'
|
||||
template: template
|
||||
|
@ -41,18 +46,29 @@ module.exports = class CampaignView extends RootView
|
|||
'click .level-info-container .start-level': 'onClickStartLevel'
|
||||
'click .level-info-container .view-solutions': 'onClickViewSolutions'
|
||||
'click #volume-button': 'onToggleVolume'
|
||||
'click .portal .campaign': 'onClickPortalCampaign'
|
||||
'mouseenter .portals': 'onMouseEnterPortals'
|
||||
'mouseleave .portals': 'onMouseLeavePortals'
|
||||
'mousemove .portals': 'onMouseMovePortals'
|
||||
|
||||
constructor: (options, @terrain='dungeon') ->
|
||||
constructor: (options, @terrain) ->
|
||||
super options
|
||||
options ?= {}
|
||||
|
||||
@campaign = new Campaign({_id:@terrain})
|
||||
@campaign = @supermodel.loadModel(@campaign, 'campaign').model
|
||||
|
||||
@editorMode = options.editorMode
|
||||
@editorMode = options?.editorMode
|
||||
if @editorMode
|
||||
@terrain ?= 'dungeon'
|
||||
else unless me.getShowsPortal()
|
||||
@terrain ?= 'dungeon'
|
||||
@levelStatusMap = {}
|
||||
@levelPlayCountMap = {}
|
||||
@sessions = @supermodel.loadCollection(new LevelSessionsCollection(), 'your_sessions', null, 0).model
|
||||
@listenToOnce @sessions, 'sync', @onSessionsLoaded
|
||||
unless @terrain
|
||||
@campaigns = @supermodel.loadCollection(new CampaignsCollection(), 'campaigns', null, 0).model
|
||||
@listenToOnce @campaigns, 'sync', @onCampaignsLoaded
|
||||
return
|
||||
|
||||
@campaign = new Campaign({_id:@terrain})
|
||||
@campaign = @supermodel.loadModel(@campaign, 'campaign').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']})
|
||||
|
@ -69,7 +85,6 @@ module.exports = class CampaignView extends RootView
|
|||
|
||||
@supermodel.loadCollection(@earnedAchievements, 'achievements')
|
||||
|
||||
@listenToOnce @sessions, 'sync', @onSessionsLoaded
|
||||
@listenToOnce @campaign, 'sync', @getLevelPlayCounts
|
||||
$(window).on 'resize', @onWindowResize
|
||||
@probablyCachedMusic = storage.load("loaded-menu-music")
|
||||
|
@ -99,6 +114,7 @@ module.exports = class CampaignView extends RootView
|
|||
@musicPlayer?.destroy()
|
||||
clearTimeout @playMusicTimeout
|
||||
@particleMan?.destroy()
|
||||
clearInterval @portalScrollInterval
|
||||
super()
|
||||
|
||||
getLevelPlayCounts: ->
|
||||
|
@ -136,31 +152,16 @@ module.exports = class CampaignView extends RootView
|
|||
getRenderData: (context={}) ->
|
||||
context = super(context)
|
||||
context.campaign = @campaign
|
||||
context.levels = _.values($.extend true, {}, @campaign.get('levels'))
|
||||
context.levelsCompleted = context.levelsTotal = 0
|
||||
for level in context.levels
|
||||
level.position ?= { x: 10, y: 10 }
|
||||
level.locked = not me.ownsLevel level.original
|
||||
level.locked = false if @levelStatusMap[level.slug] in ['started', 'complete']
|
||||
level.locked = false if @editorMode
|
||||
level.locked = false if @campaign.get('name') is 'Auditions'
|
||||
level.disabled = true if level.adminOnly and @levelStatusMap[level.slug] not in ['started', 'complete']
|
||||
level.color = 'rgb(255, 80, 60)'
|
||||
if level.requiresSubscription
|
||||
level.color = 'rgb(80, 130, 200)'
|
||||
if unlocksHero = _.find(level.rewards, 'hero')?.hero
|
||||
level.unlocksHero = unlocksHero
|
||||
if level.unlocksHero
|
||||
level.purchasedHero = level.unlocksHero in (me.get('purchased')?.heroes or [])
|
||||
level.hidden = level.locked
|
||||
unless level.disabled
|
||||
++context.levelsTotal
|
||||
++context.levelsCompleted if @levelStatusMap[level.slug] is 'complete'
|
||||
context.levels = _.values($.extend true, {}, @campaign?.get('levels') ? {})
|
||||
@annotateLevel level for level in context.levels
|
||||
count = @countLevels context.levels
|
||||
context.levelsCompleted = count.completed
|
||||
context.levelsTotal = count.total
|
||||
|
||||
@determineNextLevel context.levels if @sessions.loaded
|
||||
@determineNextLevel context.levels if @sessions?.loaded
|
||||
# put lower levels in last, so in the world map they layer over one another properly.
|
||||
context.levels = (_.sortBy context.levels, (l) -> l.position.y).reverse()
|
||||
@campaign.renderedLevels = context.levels
|
||||
@campaign.renderedLevels = context.levels if @campaign
|
||||
|
||||
context.levelStatusMap = @levelStatusMap
|
||||
context.levelPlayCountMap = @levelPlayCountMap
|
||||
|
@ -168,7 +169,7 @@ module.exports = class CampaignView extends RootView
|
|||
context.mapType = _.string.slugify @terrain
|
||||
context.requiresSubscription = @requiresSubscription
|
||||
context.editorMode = @editorMode
|
||||
context.adjacentCampaigns = _.filter _.values(_.cloneDeep(@campaign.get('adjacentCampaigns') or {})), (ac) =>
|
||||
context.adjacentCampaigns = _.filter _.values(_.cloneDeep(@campaign?.get('adjacentCampaigns') or {})), (ac) =>
|
||||
return false if ac.showIfUnlocked and (ac.showIfUnlocked not in me.levels()) and not @editorMode
|
||||
ac.name = utils.i18n ac, 'name'
|
||||
styles = []
|
||||
|
@ -181,6 +182,26 @@ module.exports = class CampaignView extends RootView
|
|||
return true
|
||||
context.marked = marked
|
||||
context.i18n = utils.i18n
|
||||
|
||||
if @campaigns
|
||||
context.campaigns = {}
|
||||
for campaign in @campaigns.models
|
||||
context.campaigns[campaign.get('slug')] = campaign
|
||||
if @sessions.loaded
|
||||
levels = _.values($.extend true, {}, campaign.get('levels') ? {})
|
||||
count = @countLevels levels
|
||||
campaign.levelsTotal = count.total
|
||||
campaign.levelsCompleted = count.completed
|
||||
if campaign.get('slug') is 'dungeon'
|
||||
campaign.locked = false
|
||||
else unless campaign.levelsTotal
|
||||
campaign.locked = true
|
||||
else
|
||||
campaign.locked = true
|
||||
for campaign in @campaigns.models
|
||||
for acID, ac of campaign.get('adjacentCampaigns') ? {}
|
||||
_.find(@campaigns.models, id: acID)?.locked = false if ac.showIfUnlocked in me.levels()
|
||||
|
||||
context
|
||||
|
||||
afterRender: ->
|
||||
|
@ -203,7 +224,7 @@ module.exports = class CampaignView extends RootView
|
|||
unless window.currentModal or not @fullyRendered
|
||||
@highlightElement '.level.next', delay: 500, duration: 60000, rotation: 0, sides: ['top']
|
||||
if @editorMode
|
||||
for level in @campaign.renderedLevels
|
||||
for level in @campaign?.renderedLevels ? []
|
||||
for nextLevelOriginal in level.nextLevels ? []
|
||||
if nextLevel = _.find(@campaign.renderedLevels, original: nextLevelOriginal)
|
||||
@createLine level.position, nextLevel.position
|
||||
|
@ -220,6 +241,32 @@ module.exports = class CampaignView extends RootView
|
|||
authModal.mode = 'signup'
|
||||
@openModalView authModal
|
||||
|
||||
annotateLevel: (level) ->
|
||||
level.position ?= { x: 10, y: 10 }
|
||||
level.locked = not me.ownsLevel level.original
|
||||
level.locked = false if @levelStatusMap[level.slug] in ['started', 'complete']
|
||||
level.locked = false if @editorMode
|
||||
level.locked = false if @campaign?.get('name') is 'Auditions'
|
||||
level.disabled = true if level.adminOnly and @levelStatusMap[level.slug] not in ['started', 'complete']
|
||||
level.color = 'rgb(255, 80, 60)'
|
||||
if level.requiresSubscription
|
||||
level.color = 'rgb(80, 130, 200)'
|
||||
if unlocksHero = _.find(level.rewards, 'hero')?.hero
|
||||
level.unlocksHero = unlocksHero
|
||||
if level.unlocksHero
|
||||
level.purchasedHero = level.unlocksHero in (me.get('purchased')?.heroes or [])
|
||||
level.hidden = level.locked
|
||||
level
|
||||
|
||||
countLevels: (levels) ->
|
||||
count = total: 0, completed: 0
|
||||
for level in levels
|
||||
@annotateLevel level unless level.locked? # Annotate if we haven't already.
|
||||
unless level.disabled
|
||||
++count.total
|
||||
++count.completed if @levelStatusMap[level.slug] is 'complete'
|
||||
count
|
||||
|
||||
showLeaderboard: (levelSlug) ->
|
||||
#levelSlug ?= 'siege-of-stonehold' # Testing
|
||||
leaderboardModal = new LeaderboardModal supermodel: @supermodel, levelSlug: levelSlug
|
||||
|
@ -249,7 +296,7 @@ module.exports = class CampaignView extends RootView
|
|||
line.append($('<div class="line">')).append($('<div class="point">'))
|
||||
|
||||
applyCampaignStyles: ->
|
||||
return unless @campaign.loaded
|
||||
return unless @campaign?.loaded
|
||||
if (backgrounds = @campaign.get 'backgroundImage') and backgrounds.length
|
||||
backgrounds = _.sortBy backgrounds, 'width'
|
||||
backgrounds.reverse()
|
||||
|
@ -267,7 +314,7 @@ module.exports = class CampaignView extends RootView
|
|||
@playAmbientSound()
|
||||
|
||||
testParticles: ->
|
||||
return unless @campaign.loaded and me.getForeshadowsLevels()
|
||||
return unless @campaign?.loaded and me.getForeshadowsLevels()
|
||||
@particleMan ?= new ParticleMan()
|
||||
@particleMan.removeEmitters()
|
||||
@particleMan.attach @$el.find('.map')
|
||||
|
@ -280,12 +327,42 @@ module.exports = class CampaignView extends RootView
|
|||
continue if particleKey.length is 2 # Don't show basic levels
|
||||
@particleMan.addEmitter level.position.x / 100, level.position.y / 100, particleKey.join('-')
|
||||
|
||||
onMouseEnterPortals: (e) ->
|
||||
return unless @campaigns?.loaded and @sessions?.loaded
|
||||
@portalScrollInterval = setInterval @onMouseMovePortals, 100
|
||||
@onMouseMovePortals e
|
||||
|
||||
onMouseLeavePortals: (e) ->
|
||||
return unless @portalScrollInterval
|
||||
clearInterval @portalScrollInterval
|
||||
@portalScrollInterval = null
|
||||
|
||||
onMouseMovePortals: (e) =>
|
||||
return unless @portalScrollInterval
|
||||
$portal = @$el.find('.portal')
|
||||
$portals = @$el.find('.portals')
|
||||
if e
|
||||
@portalOffsetX = Math.round Math.max 0, e.clientX - $portal.offset().left
|
||||
bodyWidth = $('body').innerWidth()
|
||||
fraction = @portalOffsetX / bodyWidth
|
||||
return if 0.2 < fraction < 0.8
|
||||
direction = if fraction < 0.5 then 1 else -1
|
||||
magnitude = 0.2 * bodyWidth * (if direction is -1 then fraction - 0.8 else 0.2 - fraction) / 0.2
|
||||
portalsWidth = 1902 # TODO: if we add campaigns or change margins, this will get out of date...
|
||||
scrollTo = $portals.offset().left + direction * magnitude
|
||||
scrollTo = Math.max bodyWidth - portalsWidth, scrollTo
|
||||
scrollTo = Math.min 0, scrollTo
|
||||
$portals.stop().animate {marginLeft: scrollTo}, 100, 'linear'
|
||||
|
||||
onSessionsLoaded: (e) ->
|
||||
return if @editorMode
|
||||
for session in @sessions.models
|
||||
@levelStatusMap[session.get('levelID')] = if session.get('state')?.complete then 'complete' else 'started'
|
||||
@render()
|
||||
|
||||
onCampaignsLoaded: (e) ->
|
||||
@render()
|
||||
|
||||
preloadLevel: (levelSlug) ->
|
||||
levelURL = "/db/level/#{levelSlug}"
|
||||
level = new Level().setURL levelURL
|
||||
|
@ -445,3 +522,12 @@ module.exports = class CampaignView extends RootView
|
|||
@$el.find('.player-hero-icon').removeClass().addClass('player-hero-icon ' + slug)
|
||||
return
|
||||
console.error "CampaignView hero update couldn't find hero slug for original:", hero
|
||||
|
||||
onClickPortalCampaign: (e) ->
|
||||
campaign = $(e.target).closest('.campaign')
|
||||
return if campaign.is('.locked') or campaign.is('.silhouette')
|
||||
campaignSlug = campaign.data('campaign-slug')
|
||||
Backbone.Mediator.publish 'router:navigate',
|
||||
route: "/play/#{campaignSlug}"
|
||||
viewClass: CampaignView
|
||||
viewArgs: [{supermodel: @supermodel}, campaignSlug]
|
||||
|
|
|
@ -80,9 +80,8 @@ module.exports = class ControlBarView extends CocoView
|
|||
@homeLink = c.homeLink = '/play'
|
||||
@homeViewClass = 'views/play/CampaignView'
|
||||
campaign = @level.get 'campaign'
|
||||
if campaign isnt 'dungeon'
|
||||
@homeLink += '/' + campaign
|
||||
@homeViewArgs.push campaign
|
||||
@homeLink += '/' + campaign
|
||||
@homeViewArgs.push campaign
|
||||
else
|
||||
@homeLink = c.homeLink = '/'
|
||||
@homeViewClass = 'views/HomeView'
|
||||
|
|
|
@ -326,7 +326,7 @@ module.exports = class HeroVictoryModal extends ModalView
|
|||
getNextLevelLink: ->
|
||||
link = '/play'
|
||||
nextCampaign = @getNextLevelCampaign()
|
||||
link += '/' + nextCampaign unless nextCampaign is 'dungeon'
|
||||
link += '/' + nextCampaign
|
||||
link
|
||||
|
||||
onClickContinue: (e, extraOptions=null) ->
|
||||
|
|
|
@ -9,6 +9,7 @@ module.exports = function(config) {
|
|||
|
||||
// list of files / patterns to load in the browser
|
||||
files : [
|
||||
'public/javascripts/vendor.js', // need for jade definition...
|
||||
'public/javascripts/whole-vendor.js',
|
||||
'public/lib/ace/ace.js',
|
||||
'public/javascripts/aether.js',
|
||||
|
|
|
@ -81,6 +81,7 @@ AchievementSchema.post 'save', -> @constructor.loadAchievements()
|
|||
AchievementSchema.plugin(plugins.NamedPlugin)
|
||||
AchievementSchema.plugin(plugins.SearchablePlugin, {searchable: ['name']})
|
||||
AchievementSchema.plugin plugins.TranslationCoveragePlugin
|
||||
AchievementSchema.plugin plugins.PatchablePlugin
|
||||
|
||||
module.exports = Achievement = mongoose.model('Achievement', AchievementSchema, 'achievements')
|
||||
|
||||
|
|
|
@ -8,5 +8,6 @@ CampaignSchema.index({slug: 1}, {name: 'slug index', sparse: true, unique: true}
|
|||
|
||||
CampaignSchema.plugin(plugins.NamedPlugin)
|
||||
CampaignSchema.plugin(plugins.TranslationCoveragePlugin)
|
||||
CampaignSchema.plugin plugins.PatchablePlugin
|
||||
|
||||
module.exports = mongoose.model('campaign', CampaignSchema)
|
||||
|
|
|
@ -24,6 +24,15 @@ CampaignHandler = class CampaignHandler extends Handler
|
|||
hasAccess: (req) ->
|
||||
req.method is 'GET' or req.user?.isAdmin()
|
||||
|
||||
get: (req, res) ->
|
||||
return @sendForbiddenError(res) if not @hasAccess(req)
|
||||
# We don't have normal text search or anything set up to make /db/campaign work, so we'll just give them all campaigns, no problem.
|
||||
q = @modelClass.find {}
|
||||
q.exec (err, documents) =>
|
||||
return @sendDatabaseError(res, err) if err
|
||||
documents = (@formatEntity(req, doc) for doc in documents)
|
||||
@sendSuccess(res, documents)
|
||||
|
||||
getByRelationship: (req, res, args...) ->
|
||||
relationship = args[1]
|
||||
if relationship in ['levels', 'achievements']
|
||||
|
|
|
@ -34,7 +34,7 @@ module.exports = class Handler
|
|||
hasAccessToDocument: (req, document, method=null) ->
|
||||
return true if req.user?.isAdmin()
|
||||
|
||||
if @modelClass.schema.uses_coco_translation_coverage and (method or req.method).toLowerCase() is 'post'
|
||||
if @modelClass.schema.uses_coco_translation_coverage and (method or req.method).toLowerCase() in ['post', 'put']
|
||||
return true if @isJustFillingTranslations(req, document)
|
||||
|
||||
if @modelClass.schema.uses_coco_permissions
|
||||
|
|
Loading…
Reference in a new issue