diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 8f0069d6f..68bf777c3 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -52,12 +52,13 @@ players: "players" # Hover over a level on /play hours_played: "hours played" # Hover over a level on /play items: "Items" # Tooltip on item shop button from /play - unlock: "Unlock" # For purchasing items and heroes + unlock: "Unlock" # For purchasing items and heroes confirm: "Confirm" - owned: "Owned" # For items you own + owned: "Owned" # For items you own locked: "Locked" + purchasable: "Purchasable" # For a hero you unlocked but haven't purchased available: "Available" - skills_granted: "Skills Granted" # Property documentation details + skills_granted: "Skills Granted" # Property documentation details heroes: "Heroes" # Tooltip on hero shop button from /play achievements: "Achievements" # Tooltip on achievement list button from /play account: "Account" # Tooltip on account button from /play @@ -350,9 +351,11 @@ health: "Health" speed: "Speed" regeneration: "Regeneration" - range: "Range" # As in "attack or visual range" - blocks: "Blocks" # As in "this shield blocks this much damage" + range: "Range" # As in "attack or visual range" + blocks: "Blocks" # As in "this shield blocks this much damage" skills: "Skills" + available_for_purchase: "Available for Purchase" + level_to_unlock: "Level to unlock:" skill_docs: writable: "writable" # Hover over "attack" in Your Skills while playing a level to see most of this diff --git a/app/models/User.coffee b/app/models/User.coffee index 4896c98bb..eb99993d5 100644 --- a/app/models/User.coffee +++ b/app/models/User.coffee @@ -86,8 +86,8 @@ module.exports = class User extends CocoModel gemsEarned + gemsPurchased - gemsSpent heroes: -> - heroes = (me.get('earned')?.heroes ? []).concat(me.get('purchased')?.heroes ? []).concat([ThangType.heroes.captain, ThangType.heroes.knight]) - heroes = _.values ThangType.heroes if me.isAdmin() + heroes = (me.get('purchased')?.heroes ? []).concat([ThangType.heroes.captain, ThangType.heroes.knight]) + #heroes = _.values ThangType.heroes if me.isAdmin() heroes items: -> (me.get('earned')?.items ? []).concat(me.get('purchased')?.items ? []).concat([ThangType.items['simple-boots']]) levels: -> (me.get('earned')?.levels ? []).concat(me.get('purchased')?.levels ? []) diff --git a/app/schemas/models/thang_type.coffee b/app/schemas/models/thang_type.coffee index b32713a6c..75a8a6d6b 100644 --- a/app/schemas/models/thang_type.coffee +++ b/app/schemas/models/thang_type.coffee @@ -149,8 +149,9 @@ _.extend ThangTypeSchema.properties, type: 'number' description: 'Snap to this many meters in the y-direction.' components: c.array {title: 'Components', description: 'Thangs are configured by changing the Components attached to them.', uniqueItems: true, format: 'thang-components-array'}, ThangComponentSchema # TODO: uniqueness should be based on 'original', not whole thing - i18n: {type: 'object', format: 'i18n', props: ['name', 'description', 'extendedName'], description: 'Help translate this ThangType\'s name and description.'} + i18n: {type: 'object', format: 'i18n', props: ['name', 'description', 'extendedName', 'unlockLevelName'], description: 'Help translate this ThangType\'s name and description.'} extendedName: {type: 'string', title: 'Extended Hero Name', description: 'The long form of the hero\'s name. Ex.: "Captain Anya Weston".'} + unlockLevelName: {type: 'string', title: 'Unlock Level Name', description: 'The name of the level in which the hero is unlocked.'} ThangTypeSchema.required = [] diff --git a/app/styles/play/modal/play-heroes-modal.sass b/app/styles/play/modal/play-heroes-modal.sass index 3138762ef..5359c5487 100644 --- a/app/styles/play/modal/play-heroes-modal.sass +++ b/app/styles/play/modal/play-heroes-modal.sass @@ -91,8 +91,9 @@ $heroCanvasHeight: 265px left: 21px top: 18px - &.locked + &.locked:not(.purchasable) .hero-avatar + background-color: goldenrod @include filter(contrast(50%) brightness(65%)) .lock-indicator @@ -102,6 +103,10 @@ $heroCanvasHeight: 265px top: 30% @include filter(invert(90%)) + &.purchasable + .hero-avatar + background-color: goldenrod + //- Small transformations to jumble the hero icons a little @@ -142,7 +147,7 @@ $heroCanvasHeight: 265px #hero-carousel .hero-item - &.locked + &.locked:not(.purchasable) @include opacity(0.6) canvas, .hero-feature-image @@ -236,6 +241,60 @@ $heroCanvasHeight: 265px .glyphicon @include scaleXY(-1, 1) + //- Different footer states + + #purchasable-hero-explanation + position: absolute + left: 32px + top: 532px + width: 541px + height: 97px + padding: 10px 40px + + h2 + color: #333 + text-align: center + + #locked-hero-explanation + position: absolute + left: 32px + top: 527px + width: 541px + height: 102px + padding: 10px 40px + text-align: center + + h2 + color: #333 + margin-top: 12px + margin-bottom: 5px + + #purchase-hero-button + width: 209px + height: 110px + position: absolute + left: 588px + top: 522px + line-height: 100px + text-align: center + text-transform: uppercase + font-size: 24.5px + font-family: Open Sans Condensed + color: white + border: 3px solid rgb(7,65,83) + background: rgb(0,119,168) + border-radius: 0 + + &:disabled + background: rgb(72, 106, 113) + opacity: 1 + color: rgba(255,255,255, 0.4) + + > * + @include opacity(0.9) + + &:hover > * + @include opacity(1) //- Programming select box diff --git a/app/templates/game-menu/inventory-modal.jade b/app/templates/game-menu/inventory-modal.jade index 66a6e7397..8026c5399 100644 --- a/app/templates/game-menu/inventory-modal.jade +++ b/app/templates/game-menu/inventory-modal.jade @@ -9,7 +9,7 @@ span.glyphicon.glyphicon-remove #equipped - if selectedHero + if selectedHero && selectedHero.get('featureImage') img(src="/file/"+selectedHero.get('featureImage'))#hero-image for slot in ['head', 'eyes', 'neck', 'torso', 'gloves', 'wrists', 'left-hand', 'right-hand', 'waist', 'feet', 'left-ring', 'right-ring', 'minion', 'flag', 'pet', 'programming-book', 'misc-0', 'misc-1'] diff --git a/app/templates/play/modal/play-heroes-modal.jade b/app/templates/play/modal/play-heroes-modal.jade index 92c06b6b6..0ebacf09e 100644 --- a/app/templates/play/modal/play-heroes-modal.jade +++ b/app/templates/play/modal/play-heroes-modal.jade @@ -12,13 +12,13 @@ .carousel-indicator-container ol.carousel-indicators for hero, index in heroes - li(data-hero-id=hero.get('original'), title=hero.name, data-slide-to=index, data-target="#hero-carousel", class="hero-indicator hero-index-" + index + (hero.locked ? " locked" : "")) + li(data-hero-id=hero.get('original'), title=hero.name, data-slide-to=index, data-target="#hero-carousel", class="hero-indicator hero-index-" + index + (hero.locked ? " locked" : "") + (hero.purchasable ? " purchasable" : "")) .hero-avatar - if hero.locked + if hero.locked && !hero.purchasable img.lock-indicator(src="/images/pages/game-menu/lock.png", draggable="false") .carousel-inner for hero in heroes - div(class="item hero-item" + (hero.locked ? " locked" : ""), data-hero-id=hero.get('original')) + div(class="item hero-item" + (hero.locked ? " locked" : "") + (hero.purchasable ? " purchasable" : ""), data-hero-id=hero.get('original')) canvas.hero-canvas .hero-feature-image img(draggable="false") @@ -28,7 +28,7 @@ .hero-stat-row .stat-label(data-i18n='choose_hero.status') - .stat-value(data-i18n=hero.locked ? 'play.locked' : 'play.available') + .stat-value(data-i18n=hero.purchasable ? 'play.purchasable' : (hero.locked ? 'play.locked' : 'play.available')) .hero-stat-row .stat-label(data-i18n='choose_hero.weapons') @@ -50,14 +50,33 @@ span.glyphicon.glyphicon-play a.right(role="button", data-slide="next", href="#hero-carousel") span.glyphicon.glyphicon-play - - - .form - .form-group.select-group - span.help-block(data-i18n="choose_hero.programming_language_description") Which programming language do you want to use? - //label.control-label(for="option-code-language", data-i18n="choose_hero.programming_language") Programming Language - select#option-code-language(name="code-language") - for option in codeLanguages - option(value=option.id, selected=codeLanguage === option.id)= option.name - - a#confirm-button(data-i18n=confirmButtonI18N) + + #hero-footer + if visibleHero + if visibleHero.purchasable + #purchasable-hero-explanation + h2(data-i18n="choose_hero.available_for_purchase") Available for Purchase + button.btn.unlock-button#purchase-hero-button + span.spr(data-i18n="play.unlock") Unlock + span= visibleHero.get('gems') + span.gem.gem-20 + + else if visibleHero.locked + //#locked-hero-explanation= lockedExplanation + #locked-hero-explanation + h2 + span= visibleHero.name + span.spl(data-i18n="play.locked") Locked + span.spr(data-i18n="choose_hero.level_to_unlock") Level to unlock: + strong= visibleHero.unlockLevelName || '???' + + else if visibleHero.loaded + .form + .form-group.select-group + span.help-block(data-i18n="choose_hero.programming_language_description") Which programming language do you want to use? + //label.control-label(for="option-code-language", data-i18n="choose_hero.programming_language") Programming Language + select#option-code-language(name="code-language") + for option in codeLanguages + option(value=option.id, selected=codeLanguage === option.id)= option.name + + a#confirm-button(data-i18n=confirmButtonI18N) diff --git a/app/views/i18n/I18NEditThangTypeView.coffee b/app/views/i18n/I18NEditThangTypeView.coffee index 70698bd55..1c3c07012 100644 --- a/app/views/i18n/I18NEditThangTypeView.coffee +++ b/app/views/i18n/I18NEditThangTypeView.coffee @@ -14,3 +14,4 @@ module.exports = class ThangTypeI18NView extends I18NEditModelView @wrapRow('Name', ['name'], name, i18n[lang]?.name, []) @wrapRow('Description', ['description'], @model.get('description'), i18n[lang]?.description, [], 'markdown') @wrapRow('Extended Hero Name', ['extendedName'], @model.get('extendedName'), i18n[lang]?.extendedName, []) + @wrapRow('Unlock Level Name', ['unlockLevelName'], @model.get('unlockLevelName'), i18n[lang]?.unlockLevelName, []) diff --git a/app/views/play/modal/PlayHeroesModal.coffee b/app/views/play/modal/PlayHeroesModal.coffee index c4f98019a..6400a9406 100644 --- a/app/views/play/modal/PlayHeroesModal.coffee +++ b/app/views/play/modal/PlayHeroesModal.coffee @@ -28,23 +28,28 @@ module.exports = class PlayHeroesModal extends ModalView @confirmButtonI18N = options.confirmButtonI18N ? "common.save" @heroes = new CocoCollection([], {model: ThangType}) @heroes.url = '/db/thang.type?view=heroes' - @heroes.setProjection ['original','name','slug','soundTriggers','featureImage','gems','heroClass','description','components','extendedName','i18n'] + @heroes.setProjection ['original','name','slug','soundTriggers','featureImage','gems','heroClass','description','components','extendedName','unlockLevelName','i18n'] @heroes.comparator = 'gems' @listenToOnce @heroes, 'sync', @onHeroesLoaded @supermodel.loadCollection(@heroes, 'heroes') @stages = {} @session = options.session @initCodeLanguageList options.hadEverChosenHero + @heroAnimationInterval = setInterval @animateHeroes, 2500 onHeroesLoaded: -> - for hero in @heroes.models - hero.name = utils.i18n hero.attributes, 'extendedName' # or whatever the property name ends up being - hero.name ?= utils.i18n hero.attributes, 'name' - hero.description = utils.i18n hero.attributes, 'description' - original = hero.get('original') - hero.locked = original not in [ThangType.heroes.captain, ThangType.heroes.knight] and not me.ownsHero(original) - hero.class = (hero.get('heroClass') or 'warrior').toLowerCase() - hero.stats = hero.getHeroStats() + @formatHero hero for hero in @heroes.models + + formatHero: (hero) -> + hero.name = utils.i18n hero.attributes, 'extendedName' # or whatever the property name ends up being + hero.name ?= utils.i18n hero.attributes, 'name' + hero.description = utils.i18n hero.attributes, 'description' + hero.unlockLevelName = utils.i18n hero.attributes, 'unlockLevelName' + original = hero.get('original') + hero.locked = not me.ownsHero(original) + hero.purchasable = hero.locked and (original in (me.get('earned')?.heroes ? [])) + hero.class = (hero.get('heroClass') or 'warrior').toLowerCase() + hero.stats = hero.getHeroStats() getRenderData: (context={}) -> context = super(context) @@ -53,6 +58,7 @@ module.exports = class PlayHeroesModal extends ModalView context.codeLanguages = @codeLanguageList context.codeLanguage = @codeLanguage = @options?.session?.get('codeLanguage') ? me.get('aceConfig')?.language ? 'python' context.confirmButtonI18N = @confirmButtonI18N + context.visibleHero = @visibleHero context afterRender: -> @@ -73,6 +79,11 @@ module.exports = class PlayHeroesModal extends ModalView @buildCodeLanguages() Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'game-menu-open', volume: 1 + rerenderFooter: -> + @formatHero @visibleHero + @renderSelectors '#hero-footer' + @buildCodeLanguages() + initCodeLanguageList: (hadEverChosenHero) -> @codeLanguageList = [ {id: 'python', name: "Python (#{$.i18n.t('choose_hero.default')})"} @@ -93,7 +104,8 @@ module.exports = class PlayHeroesModal extends ModalView @preloadHero heroIndex + 1 @preloadHero heroIndex - 1 @selectedHero = hero unless hero.locked - $('#choose-inventory-button').prop 'disabled', hero.locked + @visibleHero = hero + @rerenderFooter() @trigger 'hero-loaded', {hero: hero} getFullHero: (original) -> @@ -111,7 +123,8 @@ module.exports = class PlayHeroesModal extends ModalView loadHero: (hero, heroIndex, preloading=false) -> createjs.Ticker.removeEventListener 'tick', stage for stage in _.values @stages - if featureImage = hero.get 'featureImage' + # TODO: make sure we are going to axe featureImage, then remove this + if false and featureImage = hero.get 'featureImage' $(".hero-item[data-hero-id='#{hero.get('original')}'] canvas").hide() $(".hero-item[data-hero-id='#{hero.get('original')}'] .hero-feature-image").show().find('img').prop('src', '/file/' + featureImage) @playSelectionSound hero unless preloading @@ -129,27 +142,53 @@ module.exports = class PlayHeroesModal extends ModalView builder = new SpriteBuilder(fullHero) movieClip = builder.buildMovieClip(fullHero.get('actions').attack?.animation ? fullHero.get('actions').idle.animation) movieClip.scaleX = movieClip.scaleY = canvas.prop('height') / 120 # Average hero height is ~110px tall at normal resolution - if fullHero.get('name') in ['Knight', 'Robot Walker'] # These are too big, so shrink them. - movieClip.scaleX *= 0.7 - movieClip.scaleY *= 0.7 movieClip.regX = -fullHero.get('positions').registration.x movieClip.regY = -fullHero.get('positions').registration.y movieClip.x = canvas.prop('width') * 0.5 movieClip.y = canvas.prop('height') * 0.925 # This is where the feet go. + if fullHero.get('name') is 'Knight' + movieClip.scaleX *= 0.7 + movieClip.scaleY *= 0.7 + if fullHero.get('name') is 'Potion Master' + movieClip.scaleX *= 0.9 + movieClip.scaleY *= 0.9 + movieClip.regX *= 1.1 + movieClip.regY *= 1.4 + if fullHero.get('name') is 'Samurai' + movieClip.scaleX *= 0.7 + movieClip.scaleY *= 0.7 + movieClip.regX *= 1.2 + movieClip.regY *= 1.35 + if fullHero.get('name') is 'Librarian' + movieClip.regX *= 0.7 + movieClip.regY *= 1.2 + if fullHero.get('name') is 'Sorcerer' + movieClip.scaleX *= 0.9 + movieClip.scaleY *= 0.9 + movieClip.regX *= 1.15 + movieClip.regY *= 1.3 + stage = new createjs.Stage(canvas[0]) @stages[heroIndex] = stage stage.addChild movieClip stage.update() + movieClip.loop = false movieClip.gotoAndPlay 0 unless preloading createjs.Ticker.addEventListener 'tick', stage @playSelectionSound hero + @rerenderFooter() if fullHero.loaded _.defer onLoaded else @listenToOnce fullHero, 'sync', onLoaded fullHero + animateHeroes: => + return unless @visibleHero + heroIndex = Math.max 0, _.findIndex(@heroes.models, ((hero) => hero.get('original') is @visibleHero.get('original'))) + @stages[heroIndex]?.children?[0]?.gotoAndPlay? 0 + playSelectionSound: (hero) -> return if @$el.hasClass 'secret' @currentSoundInstance?.stop() @@ -208,6 +247,7 @@ module.exports = class PlayHeroesModal extends ModalView Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'game-menu-close', volume: 1 destroy: -> + clearInterval @heroAnimationInterval for heroIndex, stage of @stages createjs.Ticker.removeEventListener "tick", stage stage.removeAllChildren() diff --git a/server/levels/thangs/thang_type_handler.coffee b/server/levels/thangs/thang_type_handler.coffee index 537058fbd..088d6a454 100644 --- a/server/levels/thangs/thang_type_handler.coffee +++ b/server/levels/thangs/thang_type_handler.coffee @@ -31,6 +31,7 @@ ThangTypeHandler = class ThangTypeHandler extends Handler 'heroClass' 'tier' 'extendedName' + 'unlockLevelName' ] hasAccess: (req) -> diff --git a/server/queues/scoring.coffee b/server/queues/scoring.coffee index bfe3793d2..4a4c78687 100644 --- a/server/queues/scoring.coffee +++ b/server/queues/scoring.coffee @@ -22,7 +22,7 @@ connectToScoringQueue = -> queues.queueClient.registerQueue 'scoring', {}, (error, data) -> if error? then throw new Error "There was an error registering the scoring queue: #{error}" scoringTaskQueue = data - log.info 'Connected to scoring task queue!' + #log.info 'Connected to scoring task queue!' module.exports.messagesInQueueCount = (req, res) -> scoringTaskQueue.totalMessagesInQueue (err, count) -> @@ -189,10 +189,10 @@ module.exports.getTwoGames = (req, res) -> 'creator': session.creator 'totalScore': session.totalScore taskObject.sessions.push sessionInformation - console.log 'Dispatching random game between', taskObject.sessions[0].creatorName, 'and', taskObject.sessions[1].creatorName + #console.log 'Dispatching random game between', taskObject.sessions[0].creatorName, 'and', taskObject.sessions[1].creatorName sendResponseObject req, res, taskObject else - console.log "Directly simulating #{humansGameID} vs. #{ogresGameID}." + #console.log "Directly simulating #{humansGameID} vs. #{ogresGameID}." LevelSession.findOne(_id: humansGameID).select(selection).lean().exec (err, humanSession) => if err? then return errors.serverError(res, 'Couldn\'t find the human game') LevelSession.findOne(_id: ogresGameID).select(selection).lean().exec (err, ogreSession) => @@ -217,7 +217,7 @@ module.exports.getTwoGames = (req, res) -> module.exports.recordTwoGames = (req, res) -> sessions = req.body.sessions - console.log 'Recording non-chained result of', sessions?[0]?.name, sessions[0]?.metrics?.rank, 'and', sessions?[1]?.name, sessions?[1]?.metrics?.rank + #console.log 'Recording non-chained result of', sessions?[0]?.name, sessions[0]?.metrics?.rank, 'and', sessions?[1]?.name, sessions?[1]?.metrics?.rank yetiGuru = clientResponseObject: req.body, isRandomMatch: true async.waterfall [ @@ -608,7 +608,7 @@ updateMatchesInSession = (matchObject, sessionID, callback) -> updateUserSimulationCounts = (reqUserID, callback) -> incrementUserSimulationCount reqUserID, 'simulatedBy', (err) => if err? then return callback err - console.log 'Incremented user simulation count!' + #console.log 'Incremented user simulation count!' unless @isRandomMatch incrementUserSimulationCount @levelSession.creator, 'simulatedFor', callback else @@ -648,7 +648,7 @@ determineIfSessionShouldContinueAndUpdateLog = (cb) -> ratio = (updatedSession.numberOfLosses) / (totalNumberOfGamesPlayed) if ratio > 0.33 cb 'shouldn\'t continue' - console.log "Ratio(#{ratio}) is bad, ending simulation" + #console.log "Ratio(#{ratio}) is bad, ending simulation" else #console.log "Ratio(#{ratio}) is good, so continuing simulations" cb null diff --git a/server_setup.coffee b/server_setup.coffee index 7120c22db..c8cf1631e 100644 --- a/server_setup.coffee +++ b/server_setup.coffee @@ -25,7 +25,7 @@ productionLogging = (tokens, req, res) -> else if status >= 300 then color = 36 elapsed = (new Date()) - req._startTime elapsedColor = if elapsed < 500 then 90 else 31 - if (status isnt 200 and status isnt 204 and status isnt 304 and status isnt 302) or elapsed > 500 + if (status isnt 200 and status isnt 201 and status isnt 204 and status isnt 304 and status isnt 302) or elapsed > 500 return "\x1b[90m#{req.method} #{req.originalUrl} \x1b[#{color}m#{res.statusCode} \x1b[#{elapsedColor}m#{elapsed}ms\x1b[0m" null