Merge branch 'master' into production

This commit is contained in:
Nick Winter 2014-12-05 17:27:40 -08:00
commit f52c524388
49 changed files with 206 additions and 196 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 441 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 KiB

View file

@ -346,7 +346,7 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
parents: "Für Eltern"
parents_title: "Dein Kind lernt zu programmieren."
parents_blurb1: "Mit CodeCombat, lernt dein Kind richtige Programme zu schreiben. Es fängt mit einfachen Befehlen an, und schreitet ganz unmerklich zu schwierigeren Themen fort."
parents_blurb2: "Für 9.99 im Monat, bekommt es jede Woche neue Herausforderungen sowie persönlichen email support von professionellen Programmierern."
parents_blurb2: "Für 9.99 im Monat, bekommt es jede Woche neue Herausforderungen sowie persönlichen email support von professionellen Programmierern." # Please update based on new wording from en.coffee
parents_blurb3: "Kein Risiko: 100% Geld zurück Garantie, und 1-Klick Abokündigung."
subscribe_button: "Abonniere jetzt"
stripe_description: "Monatsabo"
@ -641,7 +641,7 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
character_classes_title: "Charakter Klassen"
introduction_desc_intro: "Wir haben hohe Erwartungen für CodeCombat."
introduction_desc_pref: "Wir wollen ein Ort sein, an dem sich Programmierer aller coleur treffen, um gemeinsam zu spielen und zu lernen. Wo sie andere in die wunderbare Welt des Programmierens einführen, und dabei die besten Aspekte der Programmierer-Community verkörpern. Wir können und wollen dies nicht alleine tun. Projekte wie GitHub, Stack Overflow and Linux leben von den großartigen Leuten die sie bauen und nutzen. Darum ist, "
introduction_desc_github_url: "CodeCombat komplett OpenSource"
introduction_desc_github_url: "CodeCombat ist komplett OpenSource"
introduction_desc_suf: ". Wir wollen es dir so einfach und vielfältig wie irgend möglich machen, dich an diesem Projekt zu beteiligen. Damit es genauso zu deinem Projekt wird, wie zu unserem."
introduction_desc_ending: "Wir hoffen du nimmst an unserer Party teil!"
introduction_desc_signature: "- Nick, George, Scott, Michael, Jeremy and Matt"

View file

@ -227,12 +227,6 @@
victory_rate_the_level: "Rate the level: " # Only in old-style levels.
victory_return_to_ladder: "Return to Ladder"
victory_play_continue: "Continue"
victory_play_skip: "Skip Ahead"
victory_play_next_level: "Play Next Level"
victory_play_more_practice: "More Practice"
victory_play_too_easy: "Too Easy"
victory_play_just_right: "Just Right"
victory_play_too_hard: "Too Hard"
victory_saving_progress: "Saving Progress"
victory_go_home: "Go Home" # Only in old-style levels.
victory_review: "Tell us more!" # Only in old-style levels.
@ -339,7 +333,7 @@
subscribe:
subscribe_title: "Subscribe"
levels: "Unlock 25 levels! With 5 new ones every week!"
levels: "Unlock 17 extra levels! With 5 new ones every week!"
heroes: "More powerful heroes!"
gems: "3500 bonus gems every month!"
items: "Over 250 bonus items!"
@ -925,7 +919,7 @@
email_settings_url: "your email settings"
email_description_suffix: "or through links in the emails we send, you can change your preferences and easily unsubscribe at any time."
cost_title: "Cost"
cost_description: "CodeCombat is free to play in the dungeon campaign, with a $9.99 USD/mo subscription for access to later campaigns and 3500 bonus gems per month. You can cancel with a click, and we offer a 100% money-back guarantee."
cost_description: "CodeCombat is free to play for all of its core levels, with a $9.99 USD/mo subscription for access to extra level branches and 3500 bonus gems per month. You can cancel with a click, and we offer a 100% money-back guarantee."
copyrights_title: "Copyrights and Licenses"
contributor_title: "Contributor License Agreement"
contributor_description_prefix: "All contributions, both on the site and on our GitHub repository, are subject to our"

View file

@ -346,7 +346,7 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription:
parents: "Para Educadores"
parents_title: "O teu educando vai aprender a programar."
parents_blurb1: "Com o CodeCombat, o teu educando aprende ao escrever código real. Começa por aprender comandos simples e progride para tópicos mais avançados."
parents_blurb2: "Por $9.99 USD/mês, recebe novos desafios todas as semanas e suporte pessoal, via e-mail, de programadores profissionais."
parents_blurb2: "Por $9.99 USD/mês, recebe novos desafios todas as semanas e suporte pessoal, via e-mail, de programadores profissionais." # Please update based on new wording from en.coffee
parents_blurb3: "Sem Risco: 100% de garantia de devolução do dinheiro, com anulação fácil de 1 clique."
subscribe_button: "Subscrever Agora"
stripe_description: "Subscrição Mensal"

View file

@ -346,7 +346,7 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
parents: "Для Родителей"
parents_title: "Ваш ребенок научиться программировать."
parents_blurb1: "С CodeCombat ваш ребенок учится через написание реального кода. Начиная с изучения простых команд, продолжая более продвинутыми темами."
parents_blurb2: "За $9.99/месяц они получат новые испытания каждую неделю и персональную поддержку профессиональных программистов через электронную почту."
parents_blurb2: "За $9.99/месяц они получат новые испытания каждую неделю и персональную поддержку профессиональных программистов через электронную почту." # Please update based on new wording from en.coffee
parents_blurb3: "Без риска: 100% гарантия возрата денег, возможность отписаться в 1 клик."
subscribe_button: "Подпишись сейчас"
stripe_description: "Месячная подписка"
@ -925,7 +925,7 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
email_settings_url: "ваши email настройки"
email_description_suffix: "или через ссылки в email-ах, которые мы отправляем, вы можете изменить предпочтения и легко отписаться в любой момент."
cost_title: "Стоимость"
cost_description: "В настоящее время, CodeCombat 100% бесплатен! Одной из наших главных целей является сохранить его таким, чтобы как можно больше людей могли играть, независимо от места в жизни. Если небо потемнеет, мы, возможно, введём подписки, возможно, только на некоторый контент, но нам не хотелось бы. Если повезёт, мы сможем поддерживать компанию, используя"
#cost_description: "В настоящее время, CodeCombat 100% бесплатен! Одной из наших главных целей является сохранить его таким, чтобы как можно больше людей могли играть, независимо от места в жизни. Если небо потемнеет, мы, возможно, введём подписки, возможно, только на некоторый контент, но нам не хотелось бы. Если повезёт, мы сможем поддерживать компанию, используя" # Please update based on new wording from en.coffee
copyrights_title: "Авторские права и лицензии"
contributor_title: "Лицензионное соглашение соавторов"
contributor_description_prefix: "Все вклады, как на сайте, так и на нашем репозитории GitHub, подпадают под наше"

View file

@ -102,19 +102,6 @@ module.exports = class User extends CocoModel
myHeroClasses.push heroClass for heroClass, heroSlugs of ThangType.heroClasses when _.intersection(myHeroSlugs, heroSlugs).length
myHeroClasses
getBranchingGroup: ->
return @branchingGroup if @branchingGroup
return 'all-practice' # A/B test paused for Hour of Code
group = me.get('testGroupNumber') % 4
@branchingGroup = switch group
when 0 then 'no-practice'
when 1 then 'all-practice'
when 2 then 'choice-explicit'
when 3 then 'choice-implicit'
@branchingGroup = 'choice-explicit' if me.isAdmin()
application.tracker.identify branchingGroup: @branchingGroup unless me.isAdmin()
@branchingGroup
getGemPromptGroup: ->
return @gemPromptGroup if @gemPromptGroup
group = me.get('testGroupNumber') % 8
@ -125,6 +112,13 @@ module.exports = class User extends CocoModel
application.tracker.identify gemPromptGroup: @gemPromptGroup unless me.isAdmin()
@gemPromptGroup
isPremium: ->
return false unless stripe = @get('stripe')
return true if stripe.subscriptionID
return true if stripe.free is true
return true if _.isString(stripe.free) and new Date() < new Date(stripe.free)
return false
tiersByLevel = [-1, 0, 0.05, 0.14, 0.18, 0.32, 0.41, 0.5, 0.64, 0.82, 0.91, 1.04, 1.22, 1.35, 1.48, 1.65, 1.78, 1.96, 2.1, 2.24, 2.38, 2.55, 2.69, 2.86, 3.03, 3.16, 3.29, 3.42, 3.58, 3.74, 3.89, 4.04, 4.19, 4.32, 4.47, 4.64, 4.79, 4.96,
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 10, 10.5, 11, 11.5, 12, 12.5, 13, 13.5, 14, 14.5, 15
]

View file

@ -221,16 +221,6 @@
.last-submitted
float: none
.next-levels-prompt
display: none
margin: 30px -21px
.btn
width: 30%
width: -webkit-calc(33.333333% - 10px)
width: calc(33.333333% - 10px)
margin: 5px
.hour-of-code-done
clear: both
padding-top: 10px

View file

@ -107,9 +107,6 @@
.executed
background-color: rgba(110, 110, 110, 0.12)
.locked-code
border: 1px dashed rgba(53, 45, 34, 0.5)
+keyframes(pulseRedBackground)
from
background-color: rgba(255, 45, 27, 0.4)
@ -175,6 +172,9 @@
// Override faint gray
border-color: #BFF
.locked-code
border: 1px dashed rgba(53, 45, 34, 0.5)
// Decided it wasn't useful to show what can be hovered, since almost anything can, so we have to make it too faint to be useful if we don't want it to be really distracting.
//.ace_identifier
// border-bottom: 1px dotted rgba(0, 51, 255, 0.25)

View file

@ -231,7 +231,7 @@ $gameControlMargin: 30px
font-size: 2vw
text-shadow: 0 0 0.3vw white, 0 0 0.3vw white
&:hover
&:hover
text-decoration: none
&#forest-link
@ -240,9 +240,10 @@ $gameControlMargin: 30px
transform: rotate(-35deg)
&#dungeon-link
left: 13.01%
top: 58%
left: 9%
top: 54.5%
transform: rotate(180deg)
color: fuchsia
.game-controls
position: absolute

View file

@ -42,11 +42,6 @@ block modal-body-content
img(src=item.getPortraitURL())
.reward-text= animate ? 'New Item' : item.get('name')
.next-levels-prompt
for button in continueButtons
- var enabled = Boolean(button.link != '/play' || me.getBranchingGroup() == 'choice-implicit' || button.key == 'continue');
button.btn.btn-success.btn-lg.world-map-button.next-level-branch-button(data-href=button.link, disabled=!enabled, data-dismiss="modal", data-i18n="play_level.victory_play_" + button[me.getBranchingGroup()], data-branch-key=button.key)
block modal-footer-content
if me.get('anonymous')
p.sign-up-poke.hide

View file

@ -16,18 +16,17 @@
#selling-points
#point-levels.point
.blurb(data-i18n="subscribe.levels") 25 more levels, with 5 new levels every week!
.blurb(data-i18n="subscribe.levels")
#point-heroes.point
.blurb(data-i18n="subscribe.heroes") Unlock more heroes, including wizards and rangers!
.blurb(data-i18n="subscribe.heroes")
#point-gems.point
.blurb(data-i18n="subscribe.gems") Subscribers get 3500 bonus gems per month!
.blurb(data-i18n="subscribe.gems")
#point-items.point
.blurb(data-i18n="subscribe.items") Unlock the coding power of 275 new items!
.blurb(data-i18n="subscribe.items")
#parents-info(data-i18n="subscribe.parents")
button.btn.btn-lg.btn-illustrated.purchase-button(data-i18n="subscribe.subscribe_button")
span $9.99/mo - Subscribe
if state === 'declined'
#declined-alert.alert.alert-danger.alert-dismissible

View file

@ -8,7 +8,7 @@
- var seenNext = nextLevel;
each level in campaign.levels
if !level.hidden
- var next = level.id == nextLevel || (!seenNext && levelStatusMap[level.id] != "complete" && !level.locked && !level.disabled && (!level.practice || me.getBranchingGroup() == 'all-practice'));
- var next = level.id == nextLevel || (!seenNext && levelStatusMap[level.id] != "complete" && !level.locked && !level.disabled);
- 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)' : ''))
a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.id}", disabled=level.disabled, data-level-id=level.id, data-level-path=level.levelPath || 'level', data-level-name=level.name)
@ -73,15 +73,15 @@ button.btn.btn-lg.btn-inverse#volume-button(title="Adjust volume")
.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")
//h1#campaign-status
// if mapType == 'dungeon'
// span.spr(data-i18n="play.campaign_dungeon")
// else if mapType == 'forest'
// span.spr(data-i18n="play.campaign_forest")
// | -
// if requiresSubscription
// span.spl(data-i18n="play.subscription_required")
// else if mapType == 'dungeon'
// span.spl(data-i18n="play.free")
// else
// span.spl(data-i18n="play.subscribed")

View file

@ -24,7 +24,7 @@ module.exports = class PaymentsView extends RootView
c = super()
c.payments = @payments
c.subscribed = me.get('stripe')?.planID
c.active = me.get('stripe')?.subscriptionID
c.active = me.isPremium()
c
onClickStartSubscription: (e) ->

View file

@ -81,7 +81,7 @@ module.exports = class WorldMapView extends RootView
$('body').append($('<img src="http://code.org/api/hour/begin_codecombat.png" style="visibility: hidden;">'))
trackedHourOfCode = true
@requiresSubscription = @terrain isnt 'dungeon' and not me.get('stripe')?.subscriptionID
@requiresSubscription = @terrain isnt 'dungeon' and not me.isPremium()
destroy: ->
@setupManager?.destroy()
@ -118,14 +118,6 @@ module.exports = class WorldMapView extends RootView
@fullyRendered = true
@render()
@preloadTopHeroes() unless me.get('heroConfig')?.thangType
if @requiresSubscription
modal = if me.get('anonymous') then AuthModal else SubscribeModal
_.delay =>
@openModalView? new modal() unless window.currentModal
if modal is SubscribeModal
window.tracker?.trackEvent 'Show subscription modal', category: 'Subscription', label: 'world map loadded'
window.tracker?.trackPageView "subscription/show-modal", ['Google Analytics']
, 2000
onSubscribed: ->
@requiresSubscription = false
@ -144,9 +136,8 @@ module.exports = class WorldMapView extends RootView
level.locked = false if me.get('slug') is 'nick'
level.disabled = false if @levelStatusMap[level.id] in ['started', 'complete']
level.color = 'rgb(255, 80, 60)'
if level.practice
level.color = 'rgb(80, 130, 200)' unless me.getBranchingGroup() is 'all-practice'
level.hidden = true if me.getBranchingGroup() is 'no-practice'
if level.requiresSubscription
level.color = 'rgb(80, 130, 200)'
context.levelStatusMap = @levelStatusMap
context.levelPlayCountMap = @levelPlayCountMap
context.isIPadApp = application.isIPadApp
@ -161,10 +152,16 @@ module.exports = class WorldMapView extends RootView
@onWindowResize()
unless application.isIPadApp
_.defer => @$el?.find('.game-controls .btn').tooltip() # Have to defer or i18n doesn't take effect.
@$el.find('.level').tooltip()
@$el.find('.level').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()
console.log "#{$(@).data('level-id')}\n x: #{(100 * x).toFixed(2)}\n y: #{(100 * y).toFixed(2)}\n"
@$el.addClass _.string.slugify @terrain
@updateVolume()
unless window.currentModal or not @fullyRendered or @requiresSubscription
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()
@ -210,7 +207,7 @@ module.exports = class WorldMapView extends RootView
@adjustLevelInfoPosition e
@endHighlight()
else
if @requiresSubscription and not @levelStatusMap[level.id] and not level.adventurer
if level.requiresSubscription and @requiresSubscription and not @levelStatusMap[level.id] and not level.adventurer
modal = if me.get('anonymous') then AuthModal else SubscribeModal
@openModalView new modal()
if modal is SubscribeModal
@ -376,8 +373,8 @@ dungeon = [
id: 'shadow-guard'
original: '54174347844506ae0195a0b8'
description: 'Evade the Kithgard minion.'
x: 44
y: 11
x: 40.54
y: 11.03
nextLevels:
more_practice: 'kounter-kithwise'
continue: 'forgetful-gemsmith'
@ -388,33 +385,34 @@ dungeon = [
id: 'kounter-kithwise'
original: '54527a6257e83800009730c7'
description: 'Practice your evasion skills with more guards.'
x: 55
y: 11
x: 35.37
y: 20.61
nextLevels:
continue: 'crawlways-of-kithgard'
practice: true
requiresSubscription: true
}
{
name: 'Crawlways of Kithgard'
type: 'hero'
id: 'crawlways-of-kithgard'
original: '545287ef57e83800009730d5'
description: 'Dart in and grab the gemat the right moment.'
x: 36.48
y: 29.03
nextLevels:
#more_practice: 'crawlways-of-kithgard'
continue: 'forgetful-gemsmith'
practice: true
requiresSubscription: true
}
#{
# name: 'Crawlways of Kithgard'
# type: 'hero'
# # id: 'crawlways-of-kithgard'
# original: '545287ef57e83800009730d5'
# description: 'Dart in and grab the gemat the right moment.'
# x: 57
# y: 12
# nextLevels:
# continue: 'true-names'
# practice: true
#}
{
name: 'Forgetful Gemsmith'
type: 'hero'
id: 'forgetful-gemsmith'
original: '544a98f62d002f0000fe331a'
description: 'Grab even more gems as you practice moving.'
x: 66
y: 11
x: 54.98
y: 10.53
nextLevels:
continue: 'true-names'
}
@ -424,8 +422,8 @@ dungeon = [
id: 'true-names'
original: '541875da4c16460000ab990f'
description: 'Learn an enemy\'s true name to defeat it.'
x: 76
y: 13
x: 68.44
y: 10.70
nextLevels:
more_practice: 'favorable-odds'
continue: 'the-raised-sword'
@ -436,11 +434,12 @@ dungeon = [
id: 'favorable-odds'
original: '5452972f57e83800009730de'
description: 'Test out your battle skills by defeating more munchkins.'
x: 80.85
y: 16
x: 88.25
y: 14.92
nextLevels:
continue: 'the-raised-sword'
practice: true
requiresSubscription: true
}
{
name: 'The Raised Sword'
@ -448,23 +447,11 @@ dungeon = [
id: 'the-raised-sword'
original: '5418aec24c16460000ab9aa6'
description: 'Learn to equip yourself for combat.'
x: 85
y: 20
x: 81.51
y: 17.92
nextLevels:
continue: 'haunted-kithmaze'
}
#{
# name: 'The First Kithmaze'
# type: 'hero'
# id: 'the-first-kithmaze'
# original: '5418b9d64c16460000ab9ab4'
# description: 'The builders of Kithgard constructed many mazes to confuse travelers.'
# x: 78
# y: 29
# nextLevels:
# more_practice: 'descending-further'
# continue: 'the-second-kithmaze'
#}
{
name: 'Haunted Kithmaze'
type: 'hero'
@ -477,17 +464,32 @@ dungeon = [
more_practice: 'descending-further'
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:
more_practice: 'descending-further'
continue: 'the-second-kithmaze'
practice: true
requiresSubscription: true
}
{
name: 'Descending Further'
type: 'hero'
id: 'descending-further'
original: '5452a84d57e83800009730e4'
description: 'Another day, another maze.'
x: 70
y: 28
x: 61.68
y: 22.80
nextLevels:
continue: 'the-second-kithmaze'
practice: true
requiresSubscription: true
}
{
name: 'The Second Kithmaze'
@ -495,8 +497,8 @@ dungeon = [
id: 'the-second-kithmaze'
original: '5418cf256bae62f707c7e1c3'
description: 'Many have tried, few have found their way through this maze.'
x: 58
y: 23
x: 54.49
y: 26.49
nextLevels:
continue: 'dread-door'
}
@ -506,8 +508,8 @@ dungeon = [
id: 'dread-door'
original: '5418d40f4c16460000ab9ac2'
description: 'Behind a dread door lies a chest full of riches.'
x: 59
y: 32
x: 60.52
y: 33.70
nextLevels:
continue: 'known-enemy'
}
@ -562,11 +564,12 @@ dungeon = [
id: 'tactical-strike'
original: '5452cfa706a59e000067e4f5'
description: 'They\'re, uh, coming right for us! Sneak up behind them.'
x: 88.65
y: 63.06
x: 83.23
y: 52.73
nextLevels:
continue: 'the-final-kithmaze'
practice: true
requiresSubscription: true
}
{
name: 'The Final Kithmaze'
@ -574,8 +577,8 @@ dungeon = [
id: 'the-final-kithmaze'
original: '541b434e1ccc8eaae19f3c33'
description: 'To escape you must find your way through an Elder Kithman\'s maze.'
x: 83
y: 68
x: 86.95
y: 64.70
nextLevels:
more_practice: 'the-gauntlet'
continue: 'kithgard-gates'
@ -586,11 +589,12 @@ dungeon = [
id: 'the-gauntlet'
original: '5452d8b906a59e000067e4fa'
description: 'Rush for the stairs, battling foes at every turn.'
x: 84.89
y: 73.88
x: 76.50
y: 72.69
nextLevels:
continue: 'kithgard-gates'
practice: true
requiresSubscription: true
}
{
name: 'Kithgard Gates'
@ -658,6 +662,8 @@ forest = [
continue: 'thornbush-farm'
x: 33
y: 37
practice: true
requiresSubscription: true
}
{
name: 'Thornbush Farm'
@ -750,6 +756,7 @@ forest = [
continue: 'swift-dagger'
x: 38
y: 72
requiresSubscription: true
}
{
name: 'Swift Dagger'
@ -761,6 +768,7 @@ forest = [
continue: 'shrapnel'
x: 33
y: 72
requiresSubscription: true
}
{
name: 'Shrapnel'
@ -772,6 +780,7 @@ forest = [
continue: 'coinucopia'
x: 28
y: 73
requiresSubscription: true
}
# Wizard branch
@ -786,6 +795,7 @@ forest = [
x: 47
y: 71
adventurer: true
requiresSubscription: true
}
{
name: 'Touch of Death'
@ -798,6 +808,7 @@ forest = [
x: 52
y: 70
adventurer: true
requiresSubscription: true
}
{
name: 'Bonemender'
@ -810,6 +821,7 @@ forest = [
x: 58
y: 67
adventurer: true
requiresSubscription: true
}
{
@ -855,6 +867,7 @@ forest = [
continue: 'rich-forager'
x: 74.5
y: 92
requiresSubscription: true
}
{
name: 'Rich Forager'
@ -867,6 +880,7 @@ forest = [
x: 80
y: 88
adventurer: true
requiresSubscription: true
}
{
name: 'Siege of Stonehold'
@ -880,6 +894,7 @@ forest = [
x: 85.5
y: 83.5
adventurer: true
requiresSubscription: true
}
{
name: 'Multiplayer Treasure Grove'

View file

@ -21,7 +21,6 @@ module.exports = class HeroVictoryModal extends ModalView
events:
'click #continue-button': 'onClickContinue'
'click .next-level-branch-button': 'onClickNextLevelBranch'
'click .return-to-ladder-button': 'onClickReturnToLadder'
constructor: (options) ->
@ -110,13 +109,6 @@ module.exports = class HeroVictoryModal extends ModalView
c.readyToRank = @level.get('type', true) is 'hero-ladder' and @session.readyToRank()
c.level = @level
@continueLevelLink = @getNextLevelLink 'continue'
@morePracticeLevelLink = @getNextLevelLink 'more_practice'
@skipAheadLevelLink = @getNextLevelLink 'skip_ahead'
c.continueButtons = [
{key: 'skip_ahead', link: @skipAheadLevelLink, 'choice-explicit': 'skip', 'choice-implicit': 'too_easy'}
{key: 'continue', link: @continueLevelLink, 'choice-explicit': 'next_level', 'choice-implicit': 'just_right'}
{key: 'more_practice', link: @morePracticeLevelLink, 'choice-explicit': 'more_practice', 'choice-implicit': 'too_hard'}
]
elapsed = (new Date() - new Date(me.get('dateCreated')))
isHourOfCode = me.get('hourOfCode') or elapsed < 120 * 60 * 1000
@ -290,34 +282,15 @@ module.exports = class HeroVictoryModal extends ModalView
return link unless nextLevel = @getNextLevel type
"#{link}?next=#{nextLevel}"
# Branching group testing
getNextLevel: (type) ->
levelInfo = @getLevelInfoForSlug @level.get 'slug'
levelInfo?.nextLevels?[type] # 'more_practice', 'skip_ahead', 'continue'
levelInfo?.nextLevels?[type] # 'continue'; TODO: refactor to not have the object and just use single nextLevel property
onClickContinue: (e) ->
@playSound 'menu-button-click'
nextLevelLink = @continueLevelLink
if me.getBranchingGroup() is 'all-practice' and @morePracticeLevelLink
nextLevelLink = @morePracticeLevelLink
skipPrompt = me.getBranchingGroup() in ['no-practice', 'all-practice']
skipPrompt ||= not (@skipAheadLevelLink or @morePractiveLevelLink) and me.getBranchingGroup() is 'choice-explicit'
if skipPrompt
# Preserve the supermodel as we navigate back to the world map.
Backbone.Mediator.publish 'router:navigate', route: nextLevelLink, viewClass: require('views/play/WorldMapView'), viewArgs: [{supermodel: if @options.hasReceivedMemoryWarning then null else @supermodel}, @getNextLevelCampaign()]
else
# Hide everything except the buttons prompting them for which kind of next level to do
@$el.find('.modal-footer, .modal-body > *').hide()
@$el.find('.next-levels-prompt').show()
onClickNextLevelBranch: (e) ->
@playSound 'menu-button-click'
e.preventDefault()
route = $(e.target).data('href') or "/play/#{@getNextLevelCampaign()}"
application.tracker?.trackEvent 'Branch Selected', level: @level.get('slug'), label: @level.get('slug'), branch: $(e.target).data('branch-key'), branchingGroup: me.getBranchingGroup(), route: route
# Preserve the supermodel as we navigate back to world map.
Backbone.Mediator.publish 'router:navigate', route: route, viewClass: require('views/play/WorldMapView'), viewArgs: [{supermodel: if @options.hasReceivedMemoryWarning then null else @supermodel}, @getNextLevelCampaign()]
# Preserve the supermodel as we navigate back to the world map.
Backbone.Mediator.publish 'router:navigate', route: nextLevelLink, viewClass: require('views/play/WorldMapView'), viewArgs: [{supermodel: if @options.hasReceivedMemoryWarning then null else @supermodel}, @getNextLevelCampaign()]
onClickReturnToLadder: (e) ->
@playSound 'menu-button-click'

View file

@ -257,7 +257,6 @@ module.exports = class SpellView extends CocoView
# TODO: Lock default indent for an empty line?
return unless LevelOptions[@options.level.get('slug')]?.lockDefaultCode or CampaignOptions?.getOption?(@options?.level?.get?('slug'), 'lockDefaultCode')
return unless @spell.source is @spell.originalSource or force
return if @spell.language in ['javascript', 'lua', 'clojure', 'io'] # Only works for languages without closing delimeters on blocks currently
console.info 'Locking down default code.'
@ -294,16 +293,50 @@ module.exports = class SpellView extends CocoView
wrapper => orig.apply obj, args
obj[method]
if @lockedCodeMarkerID?
@aceSession.removeMarker @lockedCodeMarkerID
@lockedCodeMarkerID = null
finishRange = (row, startRow, startColumn) =>
range = new Range startRow, startColumn, row, @aceSession.getLine(row).length - 1
range.start = @aceDoc.createAnchor range.start
range.end = @aceDoc.createAnchor range.end
range.end.$insertRight = true
@readOnlyRanges.push range
# Remove previous locked code highlighting
if @lockedCodeMarkerIDs?
@aceSession.removeMarker marker for marker in @lockedCodeMarkerIDs
@lockedCodeMarkerIDs = []
# Create locked default code text ranges
@readOnlyRanges = []
lines = @aceDoc.getAllLines()
lastRow = row for line, row in lines when not /^\s*$/.test(line)
if lastRow?
@readOnlyRanges.push new Range 0, 0, lastRow, lines[lastRow].length - 1
@lockedCodeMarkerID = @aceSession.addMarker @readOnlyRanges[0], 'locked-code', 'fullLine'
if @spell.language in ['python', 'coffeescript']
# Lock contiguous section of default code
# Only works for languages without closing delimeters on blocks currently
lines = @aceDoc.getAllLines()
lastRow = row for line, row in lines when not /^\s*$/.test(line)
if lastRow?
@readOnlyRanges.push new Range 0, 0, lastRow, lines[lastRow].length - 1
# TODO: Highlighting does not work for multiple ranges
# TODO: Everything looks correct except the actual result.
# TODO: https://github.com/codecombat/codecombat/issues/1852
# else
# # Create a read-only range for each chunk of text not separated by an empty line
# startRow = startColumn = null
# for row in [0...@aceSession.getLength()]
# unless /^\s*$/.test @aceSession.getLine(row)
# unless startRow? and startColumn?
# startRow = row
# startColumn = 0
# else
# if startRow? and startColumn?
# finishRange row - 1, startRow, startColumn
# startRow = startColumn = null
# if startRow? and startColumn?
# finishRange @aceSession.getLength() - 1, startRow, startColumn
# Highlight locked ranges
for range in @readOnlyRanges
@lockedCodeMarkerIDs.push @aceSession.addMarker range, 'locked-code', 'fullLine'
# Override write operations that intersect with default code
interceptCommand @ace, 'onPaste', preventReadonly

View file

@ -72,7 +72,7 @@ LevelHandler = class LevelHandler extends Handler
Session.findOne(sessionQuery).exec (err, doc) =>
return @sendDatabaseError(res, err) if err
return @sendSuccess(res, doc) if doc?
return @sendPaymentRequiredError(res, err) if (not req.user.get('stripe')?.subscriptionID) and level.get('requiresSubscription')
return @sendPaymentRequiredError(res, err) if (not req.user.isPremium()) and level.get('requiresSubscription')
@createAndSaveNewSession sessionQuery, req, res
createAndSaveNewSession: (sessionQuery, req, res) =>

View file

@ -8,7 +8,6 @@ sendwithus = require '../sendwithus'
hipchat = require '../hipchat'
config = require '../../server_config'
request = require 'request'
stripe = require('stripe')(config.stripe.secretKey)
async = require 'async'
products = {

View file

@ -2,8 +2,6 @@
# the stripe property in the user with what's being stored in Stripe.
Handler = require '../commons/Handler'
config = require '../../server_config'
stripe = require('stripe')(config.stripe.secretKey)
discountHandler = require './discount_handler'
subscriptions = {
@ -13,17 +11,17 @@ subscriptions = {
}
class SubscriptionHandler extends Handler
logSubscriptionError: (req, msg) ->
console.warn "Subscription Error: #{req.user.get('slug')} (#{req.user._id}): '#{msg}'"
logSubscriptionError: (user, msg) ->
console.warn "Subscription Error: #{user.get('slug')} (#{user._id}): '#{msg}'"
subscribeUser: (req, user, done) ->
if (not req.user) or req.user.isAnonymous()
if (not req.user) or req.user.isAnonymous() or user.isAnonymous()
return done({res: 'You must be signed in to subscribe.', code: 403})
token = req.body.stripe.token
customerID = user.get('stripe')?.customerID
if not (token or customerID)
@logSubscriptionError(req, 'Missing stripe token or customer ID.')
@logSubscriptionError(user, 'Missing stripe token or customer ID.')
return done({res: 'Missing stripe token or customer ID.', code: 422})
if token
@ -31,15 +29,15 @@ class SubscriptionHandler extends Handler
stripe.customers.update customerID, { card: token }, (err, customer) =>
if err or not customer
# should not happen outside of test and production polluting each other
@logSubscriptionError(req, 'Cannot find customer: ', +customer.id + '\n\n' + err)
@logSubscriptionError(user, 'Cannot find customer: ', +customer.id + '\n\n' + err)
return done({res: 'Cannot find customer.', code: 404})
@checkForExistingSubscription(req, user, customer, done)
else
newCustomer = {
card: token
email: req.user.get('email')
metadata: { id: req.user._id + '', slug: req.user.get('slug') }
email: user.get('email')
metadata: { id: user._id + '', slug: user.get('slug') }
}
stripe.customers.create newCustomer, (err, customer) =>
@ -47,22 +45,22 @@ class SubscriptionHandler extends Handler
if err.type in ['StripeCardError', 'StripeInvalidRequestError']
return done({res: 'Card error', code: 402})
else
@logSubscriptionError(req, 'Stripe customer creation error. '+err)
@logSubscriptionError(user, 'Stripe customer creation error. '+err)
return done({res: 'Database error.', code: 500})
stripeInfo = _.cloneDeep(req.user.get('stripe') ? {})
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
stripeInfo.customerID = customer.id
req.user.set('stripe', stripeInfo)
req.user.save (err) =>
user.set('stripe', stripeInfo)
user.save (err) =>
if err
@logSubscriptionError(req, 'Stripe customer id save db error. '+err)
@logSubscriptionError(user, 'Stripe customer id save db error. '+err)
return done({res: 'Database error.', code: 500})
@checkForExistingSubscription(req, user, customer, done)
else
stripe.customers.retrieve(customerID, (err, customer) =>
if err
@logSubscriptionError(req, 'Stripe customer creation error. '+err)
@logSubscriptionError(user, 'Stripe customer creation error. '+err)
return done({res: 'Database error.', code: 500})
@checkForExistingSubscription(req, user, customer, done)
)
@ -79,14 +77,14 @@ class SubscriptionHandler extends Handler
# subscription a trial period that ends when the cancelled subscription would have ended.
stripe.customers.cancelSubscription subscription.customer, subscription.id, (err) =>
if err
@logSubscriptionError(req, 'Stripe cancel subscription error. '+err)
@logSubscriptionError(user, 'Stripe cancel subscription error. '+err)
return done({res: 'Database error.', code: 500})
options = { plan: 'basic', trial_end: subscription.current_period_end }
options.coupon = couponID if couponID
stripe.customers.update req.user.get('stripe').customerID, options, (err, customer) =>
stripe.customers.update user.get('stripe').customerID, options, (err, customer) =>
if err
@logSubscriptionError(req, 'Stripe customer plan setting error. '+err)
@logSubscriptionError(user, 'Stripe customer plan setting error. '+err)
return done({res: 'Database error.', code: 500})
@updateUser(req, user, customer, false, done)
@ -98,9 +96,9 @@ class SubscriptionHandler extends Handler
else
options = { plan: 'basic' }
options.coupon = couponID if couponID
stripe.customers.update req.user.get('stripe').customerID, options, (err, customer) =>
stripe.customers.update user.get('stripe').customerID, options, (err, customer) =>
if err
@logSubscriptionError(req, 'Stripe customer plan setting error. '+err)
@logSubscriptionError(user, 'Stripe customer plan setting error. '+err)
return done({res: 'Database error.', code: 500})
@updateUser(req, user, customer, true, done)
@ -123,25 +121,25 @@ class SubscriptionHandler extends Handler
user.save (err) =>
if err
@logSubscriptionError(req, 'Stripe user plan saving error. '+err)
@logSubscriptionError(user, 'Stripe user plan saving error. '+err)
return done({res: 'Database error.', code: 500})
req.user?.saveActiveUser 'subscribe'
user?.saveActiveUser 'subscribe'
return done()
unsubscribeUser: (req, user, done) ->
stripeInfo = _.cloneDeep(user.get('stripe'))
stripe.customers.cancelSubscription stripeInfo.customerID, stripeInfo.subscriptionID, { at_period_end: true }, (err) =>
if err
@logSubscriptionError(req, 'Stripe cancel subscription error. '+err)
@logSubscriptionError(user, 'Stripe cancel subscription error. '+err)
return done({res: 'Database error.', code: 500})
delete stripeInfo.planID
user.set('stripe', stripeInfo)
req.body.stripe = stripeInfo
user.save (err) =>
if err
@logSubscriptionError(req, 'User save unsubscribe error. '+err)
@logSubscriptionError(user, 'User save unsubscribe error. '+err)
return done({res: 'Database error.', code: 500})
req.user?.saveActiveUser 'unsubscribe'
user?.saveActiveUser 'unsubscribe'
return done()
module.exports = new SubscriptionHandler()

View file

@ -179,6 +179,13 @@ UserSchema.methods.register = (done) ->
delighted.addDelightedUser @
@saveActiveUser 'register'
UserSchema.methods.isPremium = ->
return false unless stripe = @get('stripe')
return true if stripe.subscriptionID
return true if stripe.free is true
return true if _.isString(stripe.free) and new Date() < new Date(stripe.free)
return false
UserSchema.statics.saveActiveUser = (id, event, done=null) ->
id = mongoose.Types.ObjectId id if _.isString id
@findById id, (err, user) ->

View file

@ -230,9 +230,19 @@ UserHandler = class UserHandler extends Handler
return @trackActivity(req, res, args[0], args[2], args[3]) if args[1] is 'track' and args[2]
return @getRemark(req, res, args[0]) if args[1] is 'remark'
return @searchForUser(req, res) if args[1] is 'admin_search'
return @getStripeInfo(req, res, args[0]) if args[1] is 'stripe'
return @sendNotFoundError(res)
super(arguments...)
getStripeInfo: (req, res, handle) ->
@getDocumentForIdOrSlug handle, (err, user) =>
return @sendNotFoundError(res) if not user
return @sendForbiddenError(res) unless req.user and (req.user.isAdmin() or req.user.get('_id').equals(user.get('_id')))
return @sendNotFoundError(res) #if not customerID = user.get('stripe')?.customerID
stripe.customers.retrieve customerID, (err, customer) =>
return @sendDatabaseError(res, err) if err
@sendSuccess(res, JSON.stringify(customer, null, '\t'))
agreeToCLA: (req, res) ->
return @sendForbiddenError(res) unless req.user
doc =

View file

@ -16,6 +16,8 @@ UserHandler = require './server/users/user_handler'
hipchat = require './server/hipchat'
global.tv4 = require 'tv4' # required for TreemaUtils to work
global.jsondiffpatch = require 'jsondiffpatch'
global.stripe = require('stripe')(config.stripe.secretKey)
productionLogging = (tokens, req, res) ->
status = res.statusCode