This commit is contained in:
George Saines 2014-11-22 15:18:23 -08:00
commit 2875cb09a5
28 changed files with 429 additions and 196 deletions

View file

@ -6,8 +6,10 @@ CampaignList = require('views/play/WorldMapView').campaigns
options =
'default':
autocompleteFontSizePx: 16
backspaceThrottle: false
'dungeon':
autocompleteFontSizePx: 20
backspaceThrottle: true
module.exports = CampaignOptions =
getCampaignForSlug: (slug) ->

View file

@ -91,7 +91,7 @@ module.exports = LevelOptions =
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'leather-tunic'}
requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate'}
restrictedGear: {feet: 'leather-boots'}
'the-first-kithmaze':
hidesRunShortcut: true
@ -108,6 +108,7 @@ module.exports = LevelOptions =
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
moveRightLoopSnippet: true
requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'}
restrictedGear: {feet: 'leather-boots'}
requiredCode: ['loop']
@ -123,6 +124,7 @@ module.exports = LevelOptions =
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
moveRightLoopSnippet: true
requiredGear: {feet: 'simple-boots', 'programming-book': 'programmaticon-i'}
restrictedGear: {feet: 'leather-boots'}
'dread-door':
@ -137,14 +139,14 @@ module.exports = LevelOptions =
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', torso: 'leather-tunic'}
requiredGear: {'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', torso: 'tarnished-bronze-breastplate'}
restrictedGear: {feet: 'leather-boots'}
'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: 'leather-tunic'}
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()/m}]
@ -153,7 +155,7 @@ module.exports = LevelOptions =
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses', torso: 'leather-tunic'}
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()/m}]
@ -162,7 +164,7 @@ module.exports = LevelOptions =
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'leather-tunic', eyes: 'crude-glasses'}
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()/m}]
'tactical-strike':
@ -170,7 +172,7 @@ module.exports = LevelOptions =
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'leather-tunic', eyes: 'crude-glasses'}
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()/m}]
'the-final-kithmaze':
@ -178,21 +180,21 @@ module.exports = LevelOptions =
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'leather-tunic', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
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()/m}]
'the-gauntlet':
hidesHUD: true
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'leather-tunic', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
requiredGear: {feet: 'simple-boots', 'right-hand': 'simple-sword', torso: 'tarnished-bronze-breastplate', 'programming-book': 'programmaticon-i', eyes: 'crude-glasses'}
restrictedGear: {feet: 'leather-boots'}
suspectCode: [{name: 'lone-find-nearest-enemy', pattern: /^[ ]*(self|this|@)?[:.]?findNearestEnemy()/m}]
'kithgard-gates':
hidesSay: true
hidesCodeToolbar: true
hidesRealTimePlayback: true
requiredGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', torso: 'leather-tunic'}
requiredGear: {feet: 'simple-boots', 'right-hand': 'crude-builders-hammer', torso: 'tarnished-bronze-breastplate'}
restrictedGear: {'right-hand': 'simple-sword'}
'defense-of-plainswood':
hidesRealTimePlayback: true
@ -217,25 +219,51 @@ module.exports = LevelOptions =
hidesCodeToolbar: 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'}
requiredCode: ['topEnemy']
'back-to-back':
hidesCodeToolbar: true
requiredGear: {feet: 'leather-boots', torso: 'leather-tunic', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'simple-sword', 'left-hand': 'wooden-shield'}
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'}
'ogre-encampment':
requiredGear: {torso: 'leather-tunic', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'simple-sword', 'left-hand': 'wooden-shield'}
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'}
'woodland-cleaver':
requiredGear: {torso: 'leather-tunic', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'long-sword', 'left-hand': 'wooden-shield', wrists: 'sundial-wristwatch', feet: 'leather-boots'}
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'}
'shield-rush':
requiredGear: {torso: 'leather-tunic', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'crude-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
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'}
# Warrior branch
'peasant-protection':
requiredGear: {torso: 'leather-tunic', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
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'}
'munchkin-swarm':
requiredGear: {torso: 'leather-tunic', waist: 'leather-belt', 'programming-book': 'programmaticon-ii', eyes: 'wooden-glasses', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield', wrists: 'sundial-wristwatch'}
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: {}
# Ranger branch
'munchkin-harvest':
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: {}
'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'}
'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'}
# Wizard branch
'arcane-ally':
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'}
'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: {}
'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'}
'coinucopia':
requiredGear: {'programming-book': 'programmaticon-i', feet: 'leather-boots', 'programming-book': 'programmaticon-ii', flag: 'basic-flags'}
restrictedGear: {}
@ -249,8 +277,11 @@ module.exports = LevelOptions =
requiredGear: {'programming-book': 'programmaticon-i', feet: 'leather-boots', 'programming-book': 'programmaticon-ii', flag: 'basic-flags', eyes: 'wooden-glasses', 'right-hand': 'crude-builders-hammer'}
restrictedGear: {'right-hand': 'long-sword'}
'rich-forager':
requiredGear: {'programming-book': 'programmaticon-i', feet: 'leather-boots', 'programming-book': 'programmaticon-ii', flag: 'basic-flags', eyes: 'wooden-glasses', torso: 'leather-tunic', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield'}
requiredGear: {'programming-book': 'programmaticon-i', feet: 'leather-boots', 'programming-book': 'programmaticon-ii', flag: 'basic-flags', eyes: 'wooden-glasses', torso: 'tarnished-bronze-breastplate', 'right-hand': 'long-sword', 'left-hand': 'bronze-shield'}
restrictedGear: {'right-hand': 'crude-builders-hammer'}
'multiplayer-treasure-grove':
requiredGear: {'programming-book': 'programmaticon-i', feet: 'leather-boots', 'programming-book': 'programmaticon-ii', flag: 'basic-flags', eyes: 'wooden-glasses', torso: 'leather-tunic'}
requiredGear: {'programming-book': 'programmaticon-i', feet: 'leather-boots', 'programming-book': 'programmaticon-ii', flag: 'basic-flags', eyes: 'wooden-glasses', torso: 'tarnished-bronze-breastplate'}
restrictedGear: {}
'siege-of-stonehold':
requiredGear: {}
restrictedGear: {}

View file

@ -342,6 +342,7 @@ module.exports = Surface = class Surface extends CocoClass
@ended = true
@setPaused true
Backbone.Mediator.publish 'surface:playback-ended', {}
@updatePaths() # TODO: this is a hack to make sure paths are on the first time the level loads
else if @currentFrame < @world.totalFrames and @ended
@ended = false
@setPaused false
@ -586,7 +587,6 @@ module.exports = Surface = class Surface extends CocoClass
updatePaths: ->
return unless @options.paths and @heroLank
return unless me.isAdmin() # TODO: Fix world thang points, targets, then remove this
@hidePaths()
return if @world.showPaths is 'never'
layerAdapter = @lankBoss.layerAdapters['Path']

View file

@ -64,7 +64,7 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
next: "Выбрать" # Go from choose hero to choose inventory before playing a level
change_hero: "Выбрать героя" # Go back from choose inventory to choose hero
choose_inventory: "Выбрать предметы"
# buy_gems: "Buy Gems"
buy_gems: "Купить самоцветы"
older_campaigns: "Старые кампании"
anonymous: "Неизвестный игрок"
level_difficulty: "Сложность: "
@ -172,7 +172,7 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
medium: "Нормально"
hard: "Сложно"
player: "Игрок"
# player_level: "Level" # Like player level 5, not like level: Dungeons of Kithgard
player_level: "Уровень" # Like player level 5, not like level: Dungeons of Kithgard
units:
second: "секунда"
@ -315,17 +315,17 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
# equip: "Equip"
# unequip: "Unequip"
# buy_gems:
# few_gems: "A few gems"
# pile_gems: "Pile of gems"
# chest_gems: "Chest of gems"
buy_gems:
few_gems: "Немного самоцветов"
pile_gems: "Кучка самоцветов"
chest_gems: "Сундук с самоцветами"
choose_hero:
choose_hero: "Выберите героя"
programming_language: "Язык программирования"
programming_language_description: "Какой язык программирования вы хотите использовать?"
# default: "Default"
# experimental: "Experimental"
default: "По умолчанию"
experimental: "Экспериментальный"
python_blurb: "Пусть простой, но мощный, Python - прекрасный язык программирования общего применения."
javascript_blurb: "Язык для Сети."
coffeescript_blurb: "Улучшенный синтаксис JavaScript."
@ -647,9 +647,9 @@ module.exports = nativeDescription: "русский", englishDescription: "Russi
diplomat_launch_url: "запуска в октябре"
diplomat_introduction_suf: "было то, что есть значительная заинтересованность в CodeCombat в других странах! Мы создаём корпус переводчиков, стремящихся превратить один набор слов в другой набор слов для максимальной доступности CodeCombat по всему миру. Если вы любите видеть контент до официального выхода и получать эти уровни для ваших соотечественников как можно скорее, этот класс для вас."
diplomat_attribute_1: "Свободное владение английским языком и языком, на который вы хотели бы переводить. При передаче сложных идей важно иметь сильную хватку в обоих!"
# diplomat_i18n_page_prefix: "You can start translating our levels by going to our"
# diplomat_i18n_page: "translations page"
# diplomat_i18n_page_suffix: ", or our interface and website on GitHub."
diplomat_i18n_page_prefix: "Вы можете начать переводить уровни, посетив нашу"
diplomat_i18n_page: "страницу переводчиков"
diplomat_i18n_page_suffix: ", или перевести наш интерфейс и сайт на GitHub."
diplomat_join_pref_github: "Найдите файл локализации вашего языка "
diplomat_github_url: "на GitHub"
diplomat_join_suf_github: ", отредактируйте его онлайн и отправьте запрос на подтверждение изменений. Кроме того, установите флажок ниже, чтобы быть в курсе новых разработок интернационализации!"

View file

@ -8,6 +8,8 @@ module.exports =
'auth:logging-in-with-facebook': c.object {}
'auth:signed-up': c.object {}
'auth:logging-out': c.object {}
'auth:logged-in-with-facebook': c.object {title: 'Facebook logged in', description: 'Published when you successfully logged in with Facebook', required: ['response']},

View file

@ -29,6 +29,9 @@ module.exports =
'modal:closed': c.object {}
'modal:open-modal-view': c.object {required: ['modalPath']},
modalPath: {type: 'string'}
'router:navigate': c.object {required: ['route']},
route: {type: 'string'}
view: {type: 'object'}
@ -49,11 +52,15 @@ module.exports =
progress: {type: 'number', minimum: 0, maximum: 1}
'buy-gems-modal:update-products': { }
'buy-gems-modal:purchase-initiated': c.object {required: ['productID']},
productID: { type: 'string' }
'stripe:received-token': c.object { required: ['token'] },
token: { type: 'object', properties: {
id: {type: 'string'}
}}
}}
'store:item-purchased': c.object {required: ['item', 'itemSlug']},
item: {type: 'object'}
itemSlug: {type: 'string'}

View file

@ -54,9 +54,12 @@
font-size: 18px
line-height: 20px
strong
strong, a
color: #FFCCAA
a
text-decoration: underline
.hud-hint
font-weight: normal
color: #ddd

View file

@ -66,7 +66,7 @@
&.disabled
@include opacity(0.8)
.ace_cursor, .executing, .ace_active-line, .ace_gutter-active-line
@include opacity(0.2)
@include opacity(0.1)
.ace_gutter
background-color: transparent

View file

@ -45,11 +45,15 @@
// color: rgb(197, 6, 11)
color: rgb(243, 169, 49)
body:not(.dialogue-view-active)
.spell-palette-popover.popover
right: 45%
min-width: 350px
.spell-palette-popover.popover
// Only those popovers which are our direct children (spell documentation)
max-width: 600px
right: 45%
min-width: 350px
&.pinned
left: auto !important

View file

@ -49,11 +49,11 @@
span.cost
img(src="/images/common/gem.png", draggable="false")
span.big-font= item.get('gems')
if item.equippable
if item.unequippable
// Temp, while we only have Warriors: prevent them from buying non-Warrior stuff
button.btn.unlock-button.big-font(data-i18n="play.unlock", disabled=!item.affordable, data-item-id=item.id)
else
span.big-font.unequippable= item.get('heroClass')
else
button.btn.unlock-button.big-font(data-i18n="play.unlock", disabled=!item.affordable, data-item-id=item.id)
.clearfix
#item-details-view

View file

@ -43,7 +43,7 @@
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
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")

View file

@ -289,5 +289,4 @@ module.exports.ItemThangTypeNode = ItemThangTypeNode = class ItemThangTypeNode e
processThangType: (thangType) ->
return unless itemComponent = _.find thangType.get('components'), {original: LevelComponent.ItemID}
return unless itemComponent.config?.slots?.length
@constructor.thangTypes.push name: thangType.get('name'), original: thangType.get('original'), slots: itemComponent.config.slots
@constructor.thangTypes.push name: thangType.get('name'), original: thangType.get('original'), slots: itemComponent.config?.slots ? ['right-hand']

View file

@ -84,7 +84,7 @@ module.exports = class InventoryModal extends ModalView
# sort into one of the four groups
locked = not (item.get('original') in me.items())
locked = false if me.get('slug') is 'nick'
#locked = false if me.get('slug') is 'nick'
if not item.getFrontFacingStats().props.length and not _.size(item.getFrontFacingStats().stats) and not locked # Temp: while there are placeholder items
null # Don't put into a collection
@ -247,6 +247,8 @@ module.exports = class InventoryModal extends ModalView
@delegateEvents()
@setUpDraggableEventsForAvailableEquipment()
@itemDetailsView.setItem(item)
Backbone.Mediator.publish 'store:item-purchased', item: item, itemSlug: item.get('slug')
else
button.addClass('confirm').text($.i18n.t('play.confirm'))
@$el.one 'click', (e) ->
@ -378,15 +380,19 @@ module.exports = class InventoryModal extends ModalView
unless itemModel and heroClass in itemModel.classes
console.log 'Unequipping', itemModel.get('heroClass'), 'item', itemModel.get('name'), 'from slot due to class restrictions.'
@unequipItemFromSlot @$el.find(".item-slot[data-slot='#{slot}']")
delete equipment[slot]
for slot, item of restrictedGear
equipped = equipment[slot]
if equipped and equipped is gear[restrictedGear[slot]]
console.log 'Unequipping restricted item', restrictedGear[slot], 'for', slot, 'before level', @options.levelID
@unequipItemFromSlot @$el.find(".item-slot[data-slot='#{slot}']")
if heroClass is 'Warrior'
delete equipment[slot]
if (heroClass is 'Warrior' or
(heroClass is 'Ranger' and @options.levelID in ['swift-dagger', 'shrapnel']) or
(heroClass is 'Wizard' and @options.levelID in ['touch-of-death', 'bonemender']))
# After they switch to a ranger or wizard, we stop being so finicky about gear.
for slot, item of requiredGear
#continue if item is 'leather-tunic' and inWorldMap and @options.levelID is 'the-raised-sword' # Don't tell them they need it until they need it in the level # ... when we make it so that you can buy it
continue if item is 'tarnished-bronze-breastplate' and inWorldMap and @options.levelID is 'the-raised-sword' # Don't tell them they need it until they need it in the level
equipped = equipment[slot]
continue if equipped and not (
(item is 'crude-builders-hammer' and equipped in [gear['simple-sword'], gear['long-sword'], gear['sharpened-sword'], gear['roughedge']]) or
@ -471,10 +477,11 @@ module.exports = class InventoryModal extends ModalView
gear =
'simple-boots': '53e237bf53457600003e3f05'
'simple-sword': '53e218d853457600003e3ebe'
'leather-tunic': '53e22eac53457600003e3efc'
'tarnished-bronze-breastplate': '53e22eac53457600003e3efc'
'leather-boots': '53e2384453457600003e3f07'
'leather-belt': '5437002a7beba4a82024a97d'
'programmaticon-i': '53e4108204c00d4607a89f78'
'programmaticon-ii': '546e25d99df4a17d0d449be1'
'crude-glasses': '53e238df53457600003e3f0b'
'crude-builders-hammer': '53f4e6e3d822c23505b74f42'
'long-sword': '544d7d1f8494308424f564a3'
@ -484,3 +491,9 @@ gear =
'basic-flags': '545bacb41e649a4495f887da'
'roughedge': '544d7d918494308424f564a7'
'sharpened-sword': '544d7deb8494308424f564ab'
'crude-crossbow': '544d7ffd8494308424f564c3'
'crude-dagger': '544d952b8494308424f56517'
'weak-charge': '544d957d8494308424f5651f'
'enchanted-stick': '544d87188494308424f564f1'
'unholy-tome-i': '546374bc3839c6e02811d308'
'book-of-life-i': '546375653839c6e02811d30b'

View file

@ -32,6 +32,7 @@ module.exports = class RootView extends CocoView
subscriptions:
'achievements:new': 'handleNewAchievements'
'modal:open-modal-view': 'onOpenModalView'
showNewAchievement: (achievement, earnedAchievement) ->
return if achievement.get('collection') is 'level.sessions'
@ -64,6 +65,10 @@ module.exports = class RootView extends CocoView
window.tracker?.trackEvent 'Homepage', Action: anchorText, ['Google Analytics'] if @id is 'home-view' and anchorText
@toggleModal e
onOpenModalView: (e) ->
return console.error "Couldn't find modalPath #{e.modalPath}" unless e.modalPath and ModalClass = require e.modalPath
@openModalView new ModalClass {}
showLoading: ($el) ->
$el ?= @$el.find('.main-content-area')
super($el)

View file

@ -90,6 +90,7 @@ module.exports = class AuthModal extends ModalView
userObject.emails.generalNews.enabled = subscribe
res = tv4.validateMultiple userObject, User.schema
return forms.applyErrorsToForm(@$el, res.errors) unless res.valid
Backbone.Mediator.publish "auth:signed-up", {}
window.tracker?.trackEvent 'Finished Signup'
@enableModalInProgress(@$el)
createUser userObject, null, window.nextLevelURL
@ -125,7 +126,7 @@ module.exports = class AuthModal extends ModalView
onClickGPlusLogin: ->
step.done = false for step in @gplusAuthSteps
handler = application.gplusHandler
@listenToOnce handler, 'logged-in', ->
@gplusAuthSteps[0].done = true
@renderGPlusAuthChecklist()
@ -141,7 +142,7 @@ module.exports = class AuthModal extends ModalView
@listenToOnce handler, 'logging-into-codecombat', ->
@gplusAuthSteps[3].done = true
@renderGPlusAuthChecklist()
renderGPlusAuthChecklist: ->
template = require 'templates/modal/auth-modal-gplus-checklist'
el = $(template({steps: @gplusAuthSteps}))

View file

@ -40,7 +40,7 @@ module.exports = class WorldMapView extends RootView
@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', ->
@ -56,7 +56,7 @@ module.exports = class WorldMapView extends RootView
earned[group].push(reward)
addedSomething = true
@supermodel.loadCollection(@earnedAchievements, 'achievements')
@listenToOnce @sessions, 'sync', @onSessionsLoaded
@getLevelPlayCounts()
$(window).on 'resize', @onWindowResize
@ -110,6 +110,7 @@ module.exports = class WorldMapView extends RootView
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.disabled = false if @levelStatusMap[level.id] in ['started', 'complete']
level.color = 'rgb(255, 80, 60)'
if level.practice
@ -136,7 +137,7 @@ module.exports = class WorldMapView extends RootView
if levelID = @$el.find('.level.next').data('level-id')
@$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show()
pos = @$el.find('.level.next').offset()
@adjustLevelInfoPosition pageX: pos.left, pageY: pos.top + 250
@adjustLevelInfoPosition pageX: pos.left, pageY: pos.top
@manuallyPositionedLevelInfoID = levelID
afterInsert: ->
@ -203,6 +204,7 @@ module.exports = class WorldMapView extends RootView
@$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show()
@adjustLevelInfoPosition e
@endHighlight()
@manuallyPositionedLevelInfoID = false
onMouseLeaveLevel: (e) ->
return if application.isIPadApp
@ -688,7 +690,9 @@ forest = [
continue: 'peasant-protection'
x: 58.54
y: 66.73
}
}
# Warrior branch
{
name: 'Peasant Protection'
type: 'hero'
@ -709,6 +713,77 @@ forest = [
x: 71.19
y: 63.61
}
# Ranger branch
{
name: 'Munchkin Harvest'
type: 'hero'
id: 'munchkin-harvest'
description: 'Join forces with a new hero: Amara Arrowhead.'
nextLevels:
continue: 'swift-dagger'
disabled: not me.isAdmin()
x: 64.37
y: 69.18
}
{
name: 'Swift Dagger'
type: 'hero'
id: 'swift-dagger'
description: 'Deal damage from a distance with your new hero.'
nextLevels:
continue: 'shrapnel'
disabled: not me.isAdmin()
x: 66
y: 75.61
}
{
name: 'Shrapnel'
type: 'hero'
id: 'shrapnel'
description: 'Explore the explosive arts.'
nextLevels:
continue: 'coinucopia'
disabled: not me.isAdmin()
x: 67
y: 81
}
# Wizard branch
{
name: 'Arcane Ally'
type: 'hero'
id: 'arcane-ally'
description: 'Stand your ground against large ogres with a new hero: Ms. Hushbaum.'
nextLevels:
continue: 'touch-of-death'
disabled: not me.isAdmin()
x: 64.37
y: 55.18
}
{
name: 'Touch of Death'
type: 'hero'
id: 'touch-of-death'
description: 'Learn your first spell to siphon life from your foes.'
nextLevels:
continue: 'bonemender'
disabled: not me.isAdmin()
x: 65
y: 48
}
{
name: 'Bonemender'
type: 'hero'
id: 'bonemender'
description: 'Cast regeneration on allied soldiers to withstand a siege.'
nextLevels:
continue: 'coinucopia'
disabled: not me.isAdmin()
x: 66
y: 40
}
{
name: 'Coinucopia'
type: 'hero'
@ -759,6 +834,17 @@ forest = [
x: 77.54
y: 25.94
}
{
name: 'Siege of Stonehold'
type: 'hero'
id: 'siege-of-stonehold'
description: 'Unlock the desert world, if you are strong enough to win this epic battle!'
#nextLevels:
# continue: ''
disabled: not me.isAdmin()
x: 77.54
y: 25.94
}
{
name: 'Multiplayer Treasure Grove'
type: 'hero-ladder'

View file

@ -15,17 +15,22 @@ module.exports = class LevelDialogueView extends CocoView
events:
'click': 'onClick'
'click a': 'onClickLink'
onClick: (e) ->
Backbone.Mediator.publish 'tome:focus-editor', {}
onFrameChanged: (e) ->
@timeProgress = e.progress
@update()
onClickLink: (e) ->
route = $(e.target).attr('href')
if route and /item-store/.test route
PlayItemsModal = require 'views/play/modal/PlayItemsModal'
@openModalView new PlayItemsModal supermodel: @supermodal
e.stopPropagation()
onSpriteDialogue: (e) ->
return unless e.message
@$el.addClass 'active speaking'
$('body').addClass('dialogue-view-active')
@setMessage e.message, e.mood, e.responses
window.tracker?.trackEvent 'Heard Sprite', {message: e.message, label: e.message}, ['Google Analytics']
@ -35,6 +40,7 @@ module.exports = class LevelDialogueView extends CocoView
onSpriteClearDialogue: ->
@$el.removeClass 'active speaking'
$('body').removeClass('dialogue-view-active')
setMessage: (message, mood, responses) ->
message = marked message

View file

@ -56,6 +56,7 @@ module.exports = class LevelHUDView extends CocoView
setThang: (thang, thangType) ->
if not thang? and not @thang? then return
if thang? and @thang? and thang.id is @thang.id then return
if thang? and @hidesHUD and thang.id isnt 'Hero Placeholder' then return # Don't let them find the names of their opponents this way
@thang = thang
@thangType = thangType
return unless @thang

View file

@ -78,6 +78,7 @@ module.exports = class PlayLevelView extends RootView
'real-time-multiplayer:left-game': 'onRealTimeMultiplayerLeftGame'
'real-time-multiplayer:manual-cast': 'onRealTimeMultiplayerCast'
'ipad:memory-warning': 'onIPadMemoryWarning'
'store:item-purchased': 'onItemPurchased'
events:
'click #level-done-button': 'onDonePressed'
@ -431,7 +432,6 @@ module.exports = class PlayLevelView extends RootView
application.tracker?.trackEvent 'Saw Victory',
level: @level.get('name')
label: @level.get('name')
getDirectFirstGroup: me.getDirectFirstGroup()
application.tracker?.trackTiming victoryTime, 'Level Victory Time', @levelID, @levelID, 100
showVictory: ->
@ -906,3 +906,13 @@ module.exports = class PlayLevelView extends RootView
onIPadMemoryWarning: (e) ->
@hasReceivedMemoryWarning = true
onItemPurchased: (e) ->
heroConfig = @session.get('heroConfig') ? {}
inventory = heroConfig.inventory ? {}
slot = e.item.getAllowedSlots()[0]
if slot and not inventory[slot]
# Open up the inventory modal so they can equip the new item
@setupManager?.destroy()
@setupManager = new LevelSetupManager({supermodel: @supermodel, levelID: @levelID, parent: @, session: @session, hadEverChosenHero: true})
@setupManager.open()

View file

@ -148,6 +148,7 @@ module.exports = class CastButtonView extends CocoView
onLeftRealTimeMultiplayerGame: (e) ->
@inRealTimeMultiplayerSession = false
# https://mixpanel.com/report/227350/segmentation/#action:segment,arb_event:'Saw%20Victory',bool_op:or,chart_type:bar,from_date:-9,segfilter:!((filter:(operand:!('Ogre%20Encampment'),operator:%3D%3D),property:level,selected_property_type:string,type:string),(property:castButtonTextGroup,selected_property_type:number,type:number)),segment_type:number,to_date:0,type:unique,unit:day
initButtonTextABTest: ->
return if me.isAdmin()
return unless $.i18n.lng() is 'en-US'

View file

@ -37,10 +37,11 @@ module.exports = class SpellPaletteEntryView extends CocoView
afterRender: ->
super()
@$el.addClass(@doc.type)
placement = -> if $('body').hasClass('dialogue-view-active') then 'top' else 'left'
@$el.popover(
animation: false
html: true
placement: 'left'
placement: placement
trigger: 'manual' # Hover, until they click, which will then pin it until unclick.
content: @docFormatter.formatPopover()
container: 'body'

View file

@ -131,13 +131,12 @@ module.exports = class SpellView extends CocoView
addCommand
name: 'toggle-playing'
bindKey: {win: 'Ctrl-P', mac: 'Command-P|Ctrl-P'}
readOnly: true
exec: -> Backbone.Mediator.publish 'level:toggle-playing', {}
addCommand
name: 'end-current-script'
bindKey: {win: 'Shift-Space', mac: 'Shift-Space'}
# passEvent: true # https://github.com/ajaxorg/ace/blob/master/lib/ace/keyboard/keybinding.js#L114
# No easy way to selectively cancel shift+space, since we don't get access to the event.
# Maybe we could temporarily set ourselves to read-only if we somehow know that a script is active?
readOnly: true
exec: =>
if @scriptRunning
Backbone.Mediator.publish 'level:shift-space-pressed', {}
@ -147,34 +146,44 @@ module.exports = class SpellView extends CocoView
addCommand
name: 'end-all-scripts'
bindKey: {win: 'Escape', mac: 'Escape'}
exec: -> Backbone.Mediator.publish 'level:escape-pressed', {}
readOnly: true
exec: ->
console.log 'esc pressed'
Backbone.Mediator.publish 'level:escape-pressed', {}
addCommand
name: 'toggle-grid'
bindKey: {win: 'Ctrl-G', mac: 'Command-G|Ctrl-G'}
readOnly: true
exec: -> Backbone.Mediator.publish 'level:toggle-grid', {}
addCommand
name: 'toggle-debug'
bindKey: {win: 'Ctrl-\\', mac: 'Command-\\|Ctrl-\\'}
readOnly: true
exec: -> Backbone.Mediator.publish 'level:toggle-debug', {}
addCommand
name: 'toggle-pathfinding'
bindKey: {win: 'Ctrl-O', mac: 'Command-O|Ctrl-O'}
readOnly: true
exec: -> Backbone.Mediator.publish 'level:toggle-pathfinding', {}
addCommand
name: 'level-scrub-forward'
bindKey: {win: 'Ctrl-]', mac: 'Command-]|Ctrl-]'}
readOnly: true
exec: -> Backbone.Mediator.publish 'level:scrub-forward', {}
addCommand
name: 'level-scrub-back'
bindKey: {win: 'Ctrl-[', mac: 'Command-[|Ctrl-]'}
readOnly: true
exec: -> Backbone.Mediator.publish 'level:scrub-back', {}
addCommand
name: 'spell-step-forward'
bindKey: {win: 'Ctrl-Alt-]', mac: 'Command-Alt-]|Ctrl-Alt-]'}
readOnly: true
exec: -> Backbone.Mediator.publish 'tome:spell-step-forward', {}
addCommand
name: 'spell-step-backward'
bindKey: {win: 'Ctrl-Alt-[', mac: 'Command-Alt-[|Ctrl-Alt-]'}
readOnly: true
exec: -> Backbone.Mediator.publish 'tome:spell-step-backward', {}
addCommand
name: 'spell-beautify'
@ -207,6 +216,37 @@ module.exports = class SpellView extends CocoView
name: 'disable-spaces'
bindKey: 'Space'
exec: => @ace.execCommand 'insertstring', ' ' unless LevelOptions[@options.level.get('slug')]?.disableSpaces
addCommand
name: 'throttle-backspaces'
bindKey: 'Backspace'
exec: =>
# Throttle the backspace speed
# Slow to 500ms when whitespace at beginning of line is first encountered
# Slow to 100ms for remaining whitespace at beginning of line
# Rough testing showed backspaces happen at 150ms when tapping.
# Backspace speed varies by system when holding, 30ms on fastest Macbook setting.
unless CampaignOptions?.getOption?(@options?.level?.get?('slug'), 'backspaceThrottle')
@ace.remove "left"
return
nowDate = Date.now()
if @aceSession.selection.isEmpty()
cursor = @ace.getCursorPosition()
line = @aceDoc.getLine(cursor.row)
if /^\s*$/.test line.substring(0, cursor.column)
@backspaceThrottleMs ?= 500
# console.log "SpellView @backspaceThrottleMs=#{@backspaceThrottleMs}"
# console.log 'SpellView lastBackspace diff', nowDate - @lastBackspace if @lastBackspace?
if not @lastBackspace? or nowDate - @lastBackspace > @backspaceThrottleMs
@backspaceThrottleMs = 100
@lastBackspace = nowDate
@ace.remove "left"
return
@backspaceThrottleMs = null
@lastBackspace = nowDate
@ace.remove "left"
fillACE: ->
@ace.setValue @spell.source
@ -253,8 +293,15 @@ module.exports = class SpellView extends CocoView
return true if doc.owner is owner
return (owner is 'this' or owner is 'more') and (not doc.owner? or doc.owner is 'this')
if doc?.snippets?[e.language]
content = doc.snippets[e.language].code
if /loop/.test(content) and LevelOptions[@options.level.get('slug')]?.moveRightLoopSnippet
# Replace default loop snippet with an embedded moveRight()
content = switch e.language
when 'python' then 'loop:\n self.moveRight()\n ${1:}'
when 'javascript' then 'loop {\n this.moveRight();\n ${1:}\n}'
else content
entry =
content: doc.snippets[e.language].code
content: content
meta: 'press tab'
name: doc.name
tabTrigger: doc.snippets[e.language].tab
@ -810,14 +857,16 @@ module.exports = class SpellView extends CocoView
onDisableControls: (e) -> @toggleControls e, false
onEnableControls: (e) -> @toggleControls e, @writable
toggleControls: (e, enabled) ->
return if @destroyed
return if e?.controls and not ('editor' in e.controls)
return if enabled is @controlsEnabled
@controlsEnabled = enabled and @writable
disabled = not enabled
$('body').focus() if disabled and $(document.activeElement).is('.ace_text-input')
wasFocused = @ace.isFocused()
@ace.setReadOnly disabled
@ace[if disabled then 'setStyle' else 'unsetStyle'] 'disabled'
@toggleBackground()
$('body').focus() if disabled and wasFocused
toggleBackground: =>
# TODO: make the background an actual background and do the CSS trick

View file

@ -90,7 +90,7 @@ module.exports = class PlayItemsModal extends ModalView
model.silhouetted = not model.owned and model.isSilhouettedItem()
model.level = model.levelRequiredForItem() if model.get('tier')?
model.unequippable = not ('Warrior' in model.getAllowedHeroClasses()) # Temp: while there are no wizards/rangers
model.comingSoon = not model.getFrontFacingStats().props.length and not _.size model.getFrontFacingStats().stats and not model.owned # Temp: while there are placeholder items
model.comingSoon = not model.getFrontFacingStats().props.length and not _.size(model.getFrontFacingStats().stats) and not model.owned # Temp: while there are placeholder items
@idToItem[model.id] = model
if needMore
@ -112,6 +112,7 @@ module.exports = class PlayItemsModal extends ModalView
@$el.find('.nano:visible').nanoScroller({alwaysVisible: true})
@itemDetailsView = new ItemDetailsView()
@insertSubView(@itemDetailsView)
@$el.find("a[href='#item-category-armor']").click() # Start on armor tab, if it's there.
onHidden: ->
super()
@ -156,6 +157,8 @@ module.exports = class PlayItemsModal extends ModalView
#- ...then rerender key bits
@renderSelectors(".item[data-item-id='#{item.id}']", "#gems-count")
@itemDetailsView.render()
Backbone.Mediator.publish 'store:item-purchased', item: item, itemSlug: item.get('slug')
else
button.addClass('confirm').text($.i18n.t('play.confirm'))
@$el.one 'click', (e) ->

View file

@ -1,6 +1,7 @@
mongoose = require 'mongoose'
jsonschema = require '../../app/schemas/models/earned_achievement'
util = require '../../app/lib/utils'
log = require 'winston'
EarnedAchievementSchema = new mongoose.Schema({
notified:
@ -68,6 +69,6 @@ EarnedAchievementSchema.statics.createForAchievement = (achievement, doc, origin
(new EarnedAchievement(earned)).save (err, doc) ->
return log.error err if err?
earnedPoints = worth
wrapUp(doc)
wrapUp(doc)
module.exports = EarnedAchievement = mongoose.model('EarnedAchievement', EarnedAchievementSchema)

View file

@ -67,7 +67,7 @@ class EarnedAchievementHandler extends Handler
if achievement.get('proportionalTo')
return @sendBadInputError(res, 'Cannot currently do this to repeatable docs...')
EarnedAchievement.createForAchievement(achievement, trigger, null, (earnedAchievementDoc) =>
@sendSuccess(res, earnedAchievementDoc.toObject())
@sendCreated(res, earnedAchievementDoc.toObject())
)
)

View file

@ -88,10 +88,13 @@ PaymentHandler = class PaymentHandler extends Handler
#- Check existence
transactionID = transaction.transaction_id
criteria = { recipient: req.user._id, 'ios.transactionID': transactionID }
criteria = { 'ios.transactionID': transactionID }
Payment.findOne(criteria).exec((err, payment) =>
if payment
unless payment.get('recipient').equals(req.user._id)
return @sendForbiddenError(res)
@recalculateGemsFor(req.user, (err) =>
return @sendDatabaseError(res, err) if err
@sendSuccess(res, @formatEntity(req, payment))

View file

@ -122,134 +122,133 @@ describe 'Achievement', ->
# TODO: Took level achievements out of this auto achievement business, so fix these tests
#describe 'Achieving Achievements', ->
# it 'wait for achievements to be loaded', (done) ->
# Achievement.loadAchievements (achievements) ->
# expect(Object.keys(achievements).length).toBe(1)
#
# loadedAchievements = Achievement.getLoadedAchievements()
# expect(Object.keys(loadedAchievements).length).toBe(1)
# done()
#
# it 'saving an object that should trigger an unlockable achievement', (done) ->
# unittest.getNormalJoe (joe) ->
# session = new LevelSession
# permissions: simplePermissions
# creator: joe._id
# level: original: 'dungeon-arena'
# session.save (err, doc) ->
# expect(err).toBeNull()
# expect(doc).toBeDefined()
# expect(doc.creator).toBe(session.creator)
# done()
#
# it 'verify that an unlockable achievement has been earned', (done) ->
# unittest.getNormalJoe (joe) ->
# EarnedAchievement.find {}, (err, docs) ->
# expect(err).toBeNull()
# expect(docs.length).toBe(1)
# achievement = docs[0]
# expect(achievement).toBeDefined()
#
# expect(achievement.get 'achievement').toBe unlockable._id
# expect(achievement.get 'user').toBe joe._id.toHexString()
# expect(achievement.get 'notified').toBeFalsy()
# expect(achievement.get 'earnedPoints').toBe unlockable.worth
# expect(achievement.get 'achievedAmount').toBeUndefined()
# expect(achievement.get 'previouslyAchievedAmount').toBeUndefined()
# done()
#
# it 'saving an object that should trigger a repeatable achievement', (done) ->
# unittest.getNormalJoe (joe) ->
# expect(joe.get 'simulatedBy').toBeFalsy()
# joe.set('simulatedBy', 2)
# joe.save (err, doc) ->
# expect(err).toBeNull()
# done()
#
# it 'verify that a repeatable achievement has been earned', (done) ->
# unittest.getNormalJoe (joe) ->
# EarnedAchievement.find {achievementName: repeatable.name}, (err, docs) ->
# expect(err).toBeNull()
# expect(docs.length).toBe(1)
# achievement = docs[0]
#
# expect(achievement.get 'achievement').toBe repeatable._id
# expect(achievement.get 'user').toBe joe._id.toHexString()
# expect(achievement.get 'notified').toBeFalsy()
# expect(achievement.get 'earnedPoints').toBe 2 * repeatable.worth
# expect(achievement.get 'achievedAmount').toBe 2
# expect(achievement.get 'previouslyAchievedAmount').toBeFalsy()
# done()
#
#
# it 'verify that the repeatable achievement with complex exp has been earned', (done) ->
# unittest.getNormalJoe (joe) ->
# EarnedAchievement.find {achievementName: diminishing.name}, (err, docs) ->
# expect(err).toBeNull()
# expect(docs.length).toBe 1
# achievement = docs[0]
#
# expect(achievement.get 'achievedAmount').toBe 2
# expect(achievement.get 'earnedPoints').toBe (Math.log(.5 * (2 + .5)) + 1) * diminishing.worth
#
# done()
describe 'Level Session Achievement', ->
it 'does not generate earned achievements automatically, they need to be created manually', (done) ->
unittest.getNormalJoe (joe) ->
session = new LevelSession
permissions: simplePermissions
creator: joe._id
level: original: 'dungeon-arena'
session.save (err, session) ->
expect(err).toBeNull()
expect(session).toBeDefined()
expect(session.creator).toBe(session.creator)
EarnedAchievement.find {}, (err, earnedAchievements) ->
expect(err).toBeNull()
expect(earnedAchievements.length).toBe(0)
json = {achievement: unlockable._id, triggeredBy: session._id, collection: 'level.sessions'}
request.post {uri: getURL('/db/earned_achievement'), json: json}, (err, res, body) ->
expect(res.statusCode).toBe(201)
expect(body.achievement).toBe unlockable._id+''
expect(body.user).toBe joe._id.toHexString()
expect(body.notified).toBeFalsy()
expect(body.earnedPoints).toBe unlockable.worth
expect(body.achievedAmount).toBeUndefined()
expect(body.previouslyAchievedAmount).toBeUndefined()
done()
#describe 'Recalculate Achievements', ->
# EarnedAchievementHandler = require '../../../server/achievements/earned_achievement_handler'
#
# it 'remove earned achievements', (done) ->
# clearModels [EarnedAchievement], (err) ->
# expect(err).toBeNull()
# EarnedAchievement.find {}, (err, earned) ->
# expect(earned.length).toBe 0
#
# User.update {}, {$set: {points: 0}}, {multi:true}, (err) ->
# expect(err).toBeNull()
# done()
#
# it 'can not be accessed by regular users', (done) ->
# loginJoe -> request.post {uri:getURL '/admin/earned_achievement/recalculate'}, (err, res, body) ->
# expect(res.statusCode).toBe 403
# done()
#
# it 'can recalculate a selection of achievements', (done) ->
# loginAdmin ->
# EarnedAchievementHandler.constructor.recalculate ['dungeon-arena-started'], ->
# EarnedAchievement.find {}, (err, earnedAchievements) ->
# expect(earnedAchievements.length).toBe 1
#
# # Recalculate again, doesn't change a thing
# EarnedAchievementHandler.constructor.recalculate ['dungeon-arena-started'], ->
# EarnedAchievement.find {}, (err, earnedAchievements) ->
# expect(earnedAchievements.length).toBe 1
#
# unittest.getNormalJoe (joe) ->
# User.findById joe.get('id'), (err, guy) ->
# expect(err).toBeNull()
# expect(guy.get 'points').toBe unlockable.worth
# done()
#
# it 'can recalculate all achievements', (done) ->
# loginAdmin ->
# Achievement.count {}, (err, count) ->
# expect(count).toBe 3
# EarnedAchievementHandler.constructor.recalculate ->
# EarnedAchievement.find {}, (err, earnedAchievements) ->
# expect(earnedAchievements.length).toBe 3
# unittest.getNormalJoe (joe) ->
# User.findById joe.get('id'), (err, guy) ->
# expect(err).toBeNull()
# expect(guy.get 'points').toBe unlockable.worth + 2 * repeatable.worth + (Math.log(.5 * (2 + .5)) + 1) * diminishing.worth
# done()
#
# it 'cleaning up test: deleting all Achievements and related', (done) ->
# clearModels [Achievement, EarnedAchievement, LevelSession], (err) ->
# expect(err).toBeNull()
#
# # reset achievements in memory as well
# Achievement.resetAchievements()
# loadedAchievements = Achievement.getLoadedAchievements()
# expect(Object.keys(loadedAchievements).length).toBe(0)
#
# done()
describe 'Achieving Achievements', ->
it 'wait for achievements to be loaded', (done) ->
Achievement.loadAchievements (achievements) ->
expect(Object.keys(achievements).length).toBe(1)
loadedAchievements = Achievement.getLoadedAchievements()
expect(Object.keys(loadedAchievements).length).toBe(1)
done()
it 'saving an object that should trigger a repeatable achievement', (done) ->
unittest.getNormalJoe (joe) ->
expect(joe.get 'simulatedBy').toBeFalsy()
joe.set('simulatedBy', 2)
joe.save (err, doc) ->
expect(err).toBeNull()
done()
it 'verify that a repeatable achievement has been earned', (done) ->
unittest.getNormalJoe (joe) ->
EarnedAchievement.find {achievementName: repeatable.name}, (err, docs) ->
expect(err).toBeNull()
expect(docs.length).toBe(1)
achievement = docs[0]
expect(achievement.get 'achievement').toBe repeatable._id
expect(achievement.get 'user').toBe joe._id.toHexString()
expect(achievement.get 'notified').toBeFalsy()
expect(achievement.get 'earnedPoints').toBe 2 * repeatable.worth
expect(achievement.get 'achievedAmount').toBe 2
expect(achievement.get 'previouslyAchievedAmount').toBeFalsy()
done()
it 'verify that the repeatable achievement with complex exp has been earned', (done) ->
unittest.getNormalJoe (joe) ->
EarnedAchievement.find {achievementName: diminishing.name}, (err, docs) ->
expect(err).toBeNull()
expect(docs.length).toBe 1
achievement = docs[0]
expect(achievement.get 'achievedAmount').toBe 2
expect(achievement.get 'earnedPoints').toBe (Math.log(.5 * (2 + .5)) + 1) * diminishing.worth
done()
describe 'Recalculate Achievements', ->
EarnedAchievementHandler = require '../../../server/achievements/earned_achievement_handler'
it 'remove earned achievements', (done) ->
clearModels [EarnedAchievement], (err) ->
expect(err).toBeNull()
EarnedAchievement.find {}, (err, earned) ->
expect(earned.length).toBe 0
User.update {}, {$set: {points: 0}}, {multi:true}, (err) ->
expect(err).toBeNull()
done()
it 'can not be accessed by regular users', (done) ->
loginJoe -> request.post {uri:getURL '/admin/earned_achievement/recalculate'}, (err, res, body) ->
expect(res.statusCode).toBe 403
done()
it 'can recalculate a selection of achievements', (done) ->
loginAdmin ->
EarnedAchievementHandler.constructor.recalculate ['dungeon-arena-started'], ->
EarnedAchievement.find {}, (err, earnedAchievements) ->
expect(earnedAchievements.length).toBe 1
# Recalculate again, doesn't change a thing
EarnedAchievementHandler.constructor.recalculate ['dungeon-arena-started'], ->
EarnedAchievement.find {}, (err, earnedAchievements) ->
expect(earnedAchievements.length).toBe 1
unittest.getNormalJoe (joe) ->
User.findById joe.get('id'), (err, guy) ->
expect(err).toBeNull()
expect(guy.get 'points').toBe unlockable.worth
done()
it 'can recalculate all achievements', (done) ->
loginAdmin ->
Achievement.count {}, (err, count) ->
expect(count).toBe 3
EarnedAchievementHandler.constructor.recalculate ->
EarnedAchievement.find {}, (err, earnedAchievements) ->
expect(earnedAchievements.length).toBe 3
unittest.getNormalJoe (joe) ->
User.findById joe.get('id'), (err, guy) ->
expect(err).toBeNull()
expect(guy.get 'points').toBe unlockable.worth + 2 * repeatable.worth + (Math.log(.5 * (2 + .5)) + 1) * diminishing.worth
done()
it 'cleaning up test: deleting all Achievements and related', (done) ->
clearModels [Achievement, EarnedAchievement, LevelSession], (err) ->
expect(err).toBeNull()
# reset achievements in memory as well
Achievement.resetAchievements()
loadedAchievements = Achievement.getLoadedAchievements()
expect(Object.keys(loadedAchievements).length).toBe(0)
done()

View file

@ -52,6 +52,12 @@ describe '/db/payment', ->
done()
)
it 'prevents other users from reusing payment receipts', (done) ->
loginSam ->
request.post {uri: paymentURL, json: firstApplePayment}, (err, res, body) ->
expect(res.statusCode).toBe 403
done()
it 'processes only the transactionID that is given', (done) ->
loginJoe ->
request.post {uri: paymentURL, json: secondApplePayment}, (err, res, body) ->