mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-04-26 14:03:28 -04:00
Merge branch 'master' of https://github.com/codecombat/codecombat
This commit is contained in:
commit
2875cb09a5
28 changed files with 429 additions and 196 deletions
app
lib
locale
schemas/subscriptions
styles/play/level
templates/play
views
server
test/server/functional
|
@ -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) ->
|
||||
|
|
|
@ -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: {}
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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: ", отредактируйте его онлайн и отправьте запрос на подтверждение изменений. Кроме того, установите флажок ниже, чтобы быть в курсе новых разработок интернационализации!"
|
||||
|
|
|
@ -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']},
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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']
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}))
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) ->
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
)
|
||||
|
||||
)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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) ->
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue