diff --git a/app/lib/CampaignOptions.coffee b/app/lib/CampaignOptions.coffee index 9762b4a9e..8fb64858a 100644 --- a/app/lib/CampaignOptions.coffee +++ b/app/lib/CampaignOptions.coffee @@ -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) -> diff --git a/app/lib/LevelOptions.coffee b/app/lib/LevelOptions.coffee index c4e0d6778..78ca21820 100644 --- a/app/lib/LevelOptions.coffee +++ b/app/lib/LevelOptions.coffee @@ -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: {} diff --git a/app/lib/surface/Surface.coffee b/app/lib/surface/Surface.coffee index 016fc81d6..195dab4b2 100644 --- a/app/lib/surface/Surface.coffee +++ b/app/lib/surface/Surface.coffee @@ -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'] diff --git a/app/locale/ru.coffee b/app/locale/ru.coffee index a0c9f6b89..2535bc95c 100644 --- a/app/locale/ru.coffee +++ b/app/locale/ru.coffee @@ -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: ", отредактируйте его онлайн и отправьте запрос на подтверждение изменений. Кроме того, установите флажок ниже, чтобы быть в курсе новых разработок интернационализации!" diff --git a/app/schemas/subscriptions/auth.coffee b/app/schemas/subscriptions/auth.coffee index fc396e4c4..3d1665ebe 100644 --- a/app/schemas/subscriptions/auth.coffee +++ b/app/schemas/subscriptions/auth.coffee @@ -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']}, diff --git a/app/schemas/subscriptions/misc.coffee b/app/schemas/subscriptions/misc.coffee index 5773dbbff..f85a036d7 100644 --- a/app/schemas/subscriptions/misc.coffee +++ b/app/schemas/subscriptions/misc.coffee @@ -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'} - }} \ No newline at end of file + }} + + 'store:item-purchased': c.object {required: ['item', 'itemSlug']}, + item: {type: 'object'} + itemSlug: {type: 'string'} diff --git a/app/styles/play/level/level-dialogue-view.sass b/app/styles/play/level/level-dialogue-view.sass index f29d65f8a..a1a01dbf8 100644 --- a/app/styles/play/level/level-dialogue-view.sass +++ b/app/styles/play/level/level-dialogue-view.sass @@ -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 diff --git a/app/styles/play/level/tome/spell.sass b/app/styles/play/level/tome/spell.sass index b3b0a82ec..39f65f6d4 100644 --- a/app/styles/play/level/tome/spell.sass +++ b/app/styles/play/level/tome/spell.sass @@ -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 diff --git a/app/styles/play/level/tome/spell_palette_entry.sass b/app/styles/play/level/tome/spell_palette_entry.sass index 93640e9ef..fdda33886 100644 --- a/app/styles/play/level/tome/spell_palette_entry.sass +++ b/app/styles/play/level/tome/spell_palette_entry.sass @@ -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 diff --git a/app/templates/play/modal/play-items-modal.jade b/app/templates/play/modal/play-items-modal.jade index 3e81ebf53..08d0d52ba 100644 --- a/app/templates/play/modal/play-items-modal.jade +++ b/app/templates/play/modal/play-items-modal.jade @@ -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 diff --git a/app/templates/play/world-map-view.jade b/app/templates/play/world-map-view.jade index 53134e321..10612fc5c 100644 --- a/app/templates/play/world-map-view.jade +++ b/app/templates/play/world-map-view.jade @@ -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") diff --git a/app/views/editor/level/treema_nodes.coffee b/app/views/editor/level/treema_nodes.coffee index 989adfa22..e04092c8b 100644 --- a/app/views/editor/level/treema_nodes.coffee +++ b/app/views/editor/level/treema_nodes.coffee @@ -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'] diff --git a/app/views/game-menu/InventoryModal.coffee b/app/views/game-menu/InventoryModal.coffee index 618af9815..356adc24a 100644 --- a/app/views/game-menu/InventoryModal.coffee +++ b/app/views/game-menu/InventoryModal.coffee @@ -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' diff --git a/app/views/kinds/RootView.coffee b/app/views/kinds/RootView.coffee index 02c080596..48f24b938 100644 --- a/app/views/kinds/RootView.coffee +++ b/app/views/kinds/RootView.coffee @@ -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) diff --git a/app/views/modal/AuthModal.coffee b/app/views/modal/AuthModal.coffee index aa4c25cad..6027b8a86 100644 --- a/app/views/modal/AuthModal.coffee +++ b/app/views/modal/AuthModal.coffee @@ -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})) diff --git a/app/views/play/WorldMapView.coffee b/app/views/play/WorldMapView.coffee index 0e0ed80b3..099fcfc19 100644 --- a/app/views/play/WorldMapView.coffee +++ b/app/views/play/WorldMapView.coffee @@ -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' diff --git a/app/views/play/level/LevelDialogueView.coffee b/app/views/play/level/LevelDialogueView.coffee index 3629ba687..f9cb4fa20 100644 --- a/app/views/play/level/LevelDialogueView.coffee +++ b/app/views/play/level/LevelDialogueView.coffee @@ -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 diff --git a/app/views/play/level/LevelHUDView.coffee b/app/views/play/level/LevelHUDView.coffee index 23a6c9617..e2f5fcb5c 100644 --- a/app/views/play/level/LevelHUDView.coffee +++ b/app/views/play/level/LevelHUDView.coffee @@ -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 diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee index 82ef103fa..3f0b759ae 100644 --- a/app/views/play/level/PlayLevelView.coffee +++ b/app/views/play/level/PlayLevelView.coffee @@ -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() diff --git a/app/views/play/level/tome/CastButtonView.coffee b/app/views/play/level/tome/CastButtonView.coffee index d425ad6db..5ed174743 100644 --- a/app/views/play/level/tome/CastButtonView.coffee +++ b/app/views/play/level/tome/CastButtonView.coffee @@ -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' diff --git a/app/views/play/level/tome/SpellPaletteEntryView.coffee b/app/views/play/level/tome/SpellPaletteEntryView.coffee index d6870656d..a66d70131 100644 --- a/app/views/play/level/tome/SpellPaletteEntryView.coffee +++ b/app/views/play/level/tome/SpellPaletteEntryView.coffee @@ -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' diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee index 16a2dd338..45fe0eb2e 100644 --- a/app/views/play/level/tome/SpellView.coffee +++ b/app/views/play/level/tome/SpellView.coffee @@ -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 diff --git a/app/views/play/modal/PlayItemsModal.coffee b/app/views/play/modal/PlayItemsModal.coffee index d0e77f8d0..fd4b9c6a5 100644 --- a/app/views/play/modal/PlayItemsModal.coffee +++ b/app/views/play/modal/PlayItemsModal.coffee @@ -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) -> diff --git a/server/achievements/EarnedAchievement.coffee b/server/achievements/EarnedAchievement.coffee index bffe0be69..5ee686989 100644 --- a/server/achievements/EarnedAchievement.coffee +++ b/server/achievements/EarnedAchievement.coffee @@ -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) diff --git a/server/achievements/earned_achievement_handler.coffee b/server/achievements/earned_achievement_handler.coffee index f49ff545a..cfb2a1f62 100644 --- a/server/achievements/earned_achievement_handler.coffee +++ b/server/achievements/earned_achievement_handler.coffee @@ -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()) ) ) diff --git a/server/payments/payment_handler.coffee b/server/payments/payment_handler.coffee index d4c60abeb..ae2393d67 100644 --- a/server/payments/payment_handler.coffee +++ b/server/payments/payment_handler.coffee @@ -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)) diff --git a/test/server/functional/achievement.spec.coffee b/test/server/functional/achievement.spec.coffee index d1390a65c..1afac63ce 100644 --- a/test/server/functional/achievement.spec.coffee +++ b/test/server/functional/achievement.spec.coffee @@ -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() diff --git a/test/server/functional/payment.spec.coffee b/test/server/functional/payment.spec.coffee index a779b8778..ec316a621 100644 --- a/test/server/functional/payment.spec.coffee +++ b/test/server/functional/payment.spec.coffee @@ -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) ->