diff --git a/app/assets/images/pages/play/modal/play-items-modal-background.png b/app/assets/images/pages/play/modal/play-items-modal-background.png new file mode 100644 index 000000000..991b1eaa2 Binary files /dev/null and b/app/assets/images/pages/play/modal/play-items-modal-background.png differ diff --git a/app/assets/images/pages/play/modal/play-items-modal-hr.png b/app/assets/images/pages/play/modal/play-items-modal-hr.png new file mode 100644 index 000000000..47ed3904f Binary files /dev/null and b/app/assets/images/pages/play/modal/play-items-modal-hr.png differ diff --git a/app/assets/images/pages/play/modal/play-items-modal-item-background.png b/app/assets/images/pages/play/modal/play-items-modal-item-background.png new file mode 100644 index 000000000..11ea27c2c Binary files /dev/null and b/app/assets/images/pages/play/modal/play-items-modal-item-background.png differ diff --git a/app/assets/images/pages/play/modal/play-items-modal-selected-item-background.png b/app/assets/images/pages/play/modal/play-items-modal-selected-item-background.png new file mode 100644 index 000000000..4525a8505 Binary files /dev/null and b/app/assets/images/pages/play/modal/play-items-modal-selected-item-background.png differ diff --git a/app/assets/images/pages/play/modal/play-items-modal-selected-tab.png b/app/assets/images/pages/play/modal/play-items-modal-selected-tab.png new file mode 100644 index 000000000..fbef30685 Binary files /dev/null and b/app/assets/images/pages/play/modal/play-items-modal-selected-tab.png differ diff --git a/app/assets/images/pages/play/modal/play-items-modal-tab-icon-accessories.png b/app/assets/images/pages/play/modal/play-items-modal-tab-icon-accessories.png new file mode 100644 index 000000000..07c321f5b Binary files /dev/null and b/app/assets/images/pages/play/modal/play-items-modal-tab-icon-accessories.png differ diff --git a/app/assets/images/pages/play/modal/play-items-modal-tab-icon-armor.png b/app/assets/images/pages/play/modal/play-items-modal-tab-icon-armor.png new file mode 100644 index 000000000..0be834a85 Binary files /dev/null and b/app/assets/images/pages/play/modal/play-items-modal-tab-icon-armor.png differ diff --git a/app/assets/images/pages/play/modal/play-items-modal-tab-icon-books.png b/app/assets/images/pages/play/modal/play-items-modal-tab-icon-books.png new file mode 100644 index 000000000..2f60882bd Binary files /dev/null and b/app/assets/images/pages/play/modal/play-items-modal-tab-icon-books.png differ diff --git a/app/assets/images/pages/play/modal/play-items-modal-tab-icon-misc.png b/app/assets/images/pages/play/modal/play-items-modal-tab-icon-misc.png new file mode 100644 index 000000000..4a1240b83 Binary files /dev/null and b/app/assets/images/pages/play/modal/play-items-modal-tab-icon-misc.png differ diff --git a/app/assets/images/pages/play/modal/play-items-modal-tab-icon-primary.png b/app/assets/images/pages/play/modal/play-items-modal-tab-icon-primary.png new file mode 100644 index 000000000..f6348d4ae Binary files /dev/null and b/app/assets/images/pages/play/modal/play-items-modal-tab-icon-primary.png differ diff --git a/app/assets/images/pages/play/modal/play-items-modal-tab-icon-secondary.png b/app/assets/images/pages/play/modal/play-items-modal-tab-icon-secondary.png new file mode 100644 index 000000000..616588e41 Binary files /dev/null and b/app/assets/images/pages/play/modal/play-items-modal-tab-icon-secondary.png differ diff --git a/app/assets/images/pages/play/modal/play-items-modal-tab.png b/app/assets/images/pages/play/modal/play-items-modal-tab.png new file mode 100644 index 000000000..185a47d6a Binary files /dev/null and b/app/assets/images/pages/play/modal/play-items-modal-tab.png differ diff --git a/app/lib/surface/CountdownScreen.coffee b/app/lib/surface/CountdownScreen.coffee index 1f0b6925d..23e35e83b 100644 --- a/app/lib/surface/CountdownScreen.coffee +++ b/app/lib/surface/CountdownScreen.coffee @@ -34,7 +34,7 @@ module.exports = class CountdownScreen extends CocoClass makeCountdownText: -> size = Math.ceil @camera.canvasHeight / 2 - text = new createjs.Text '3...', "#{size}px Bangers", '#F7B42C' + text = new createjs.Text '3...', "#{size}px Open Sans Condensed", '#F7B42C' text.shadow = new createjs.Shadow '#000', Math.ceil(@camera.canvasHeight / 300), Math.ceil(@camera.canvasHeight / 300), Math.ceil(@camera.canvasHeight / 120) text.textAlign = 'center' text.textBaseline = 'middle' diff --git a/app/lib/surface/WaitingScreen.coffee b/app/lib/surface/WaitingScreen.coffee index cc54906f3..25c2081f4 100644 --- a/app/lib/surface/WaitingScreen.coffee +++ b/app/lib/surface/WaitingScreen.coffee @@ -34,7 +34,7 @@ module.exports = class WaitingScreen extends CocoClass makeWaitingText: -> size = Math.ceil @camera.canvasHeight / 8 - text = new createjs.Text @waitingText, "#{size}px Bangers", '#F7B42C' + text = new createjs.Text @waitingText, "#{size}px Open Sans Condensed", '#F7B42C' text.shadow = new createjs.Shadow '#000', Math.ceil(@camera.canvasHeight / 300), Math.ceil(@camera.canvasHeight / 300), Math.ceil(@camera.canvasHeight / 120) text.textAlign = 'center' text.textBaseline = 'middle' diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 6b779508a..ea0c083f9 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -51,6 +51,11 @@ 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 + confirm: "Confirm" + owned: "Owned" # For items you own + locked: "Locked" + 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 @@ -104,11 +109,12 @@ recovery_sent: "Recovery email sent." items: + primary: "Primary" + secondary: "Secondary" armor: "Armor" - hands: "Hands" accessories: "Accessories" - minions: "Minions" misc: "Misc" + books: "Books" common: loading: "Loading..." @@ -306,6 +312,9 @@ attack: "Damage" # Can also translate as "Attack" health: "Health" speed: "Speed" + regeneration: "Regeneration" + range: "Range" # As in "attack or visual range" + blocks: "Blocks" # As in "this shield blocks this much damage" skills: "Skills" save_load: diff --git a/app/models/Purchase.coffee b/app/models/Purchase.coffee new file mode 100644 index 000000000..c2b9614b5 --- /dev/null +++ b/app/models/Purchase.coffee @@ -0,0 +1,16 @@ +CocoModel = require('./CocoModel') + +module.exports = class Purchase extends CocoModel + @className: "Purchase" + urlRoot: "/db/purchase" + @schema: require 'schemas/models/purchase.schema' + + @makeFor: (toPurchase) -> + purchase = new Purchase({ + recipient: me.id + purchaser: me.id + purchased: { + original: toPurchase.get('original') + collection: _.string.underscored toPurchase.constructor.className + } + }) \ No newline at end of file diff --git a/app/models/ThangType.coffee b/app/models/ThangType.coffee index 4a31875c0..1b8509a95 100644 --- a/app/models/ThangType.coffee +++ b/app/models/ThangType.coffee @@ -2,6 +2,8 @@ CocoModel = require './CocoModel' SpriteBuilder = require 'lib/sprites/SpriteBuilder' LevelComponent = require './LevelComponent' +utils = require 'lib/utils' + buildQueue = [] module.exports = class ThangType extends CocoModel @@ -328,14 +330,30 @@ module.exports = class ThangType extends CocoModel props: props, stats: stats formatStatDisplay: (name, modifiers) -> - name = {maxHealth: 'Health', maxSpeed: 'Speed', healthReplenishRate: 'Regeneration'}[name] ? name - name = _.string.humanize name + i18nKey = { + maxHealth: 'health' + maxSpeed: 'speed' + healthReplenishRate: 'regeneration' + attackDamage: 'attack' + attackRange: 'range' + shieldDefenseFactor: 'blocks' + visualRange: 'range' + }[name] + + if i18nKey + name = $.i18n.t 'choose_hero.' + i18nKey + else + name = _.string.humanize name + format = '' - format = 'm' if /(range|radius|distance)$/i.test name + format = 'm' if /(range|radius|distance|vision)$/i.test name format ||= 's' if /cooldown$/i.test name format ||= 'm/s' if /speed$/i.test name format ||= '/s' if /(regeneration| rate)$/i.test name value = modifiers.setTo + if /(blocks)$/i.test name + format ||= '%' + value = (value*100).toFixed(1) value = value.join ', ' if _.isArray value display = [] display.push "#{value}#{format}" if value? diff --git a/app/models/User.coffee b/app/models/User.coffee index e7899b292..4f51bff84 100644 --- a/app/models/User.coffee +++ b/app/models/User.coffee @@ -67,15 +67,16 @@ module.exports = class User extends CocoModel gems: -> gemsEarned = @get('earned')?.gems ? 0 - purchased = @get('purchased') ? {} - gemsPurchased = purchased.gems ? 0 - sum = (arr) -> arr?.reduce((a, b) -> a + b) ? 0 - gemsSpent = sum(purchased.heroes) + sum(purchased.items) + sum(purchased.levels) + gemsPurchased = @get('purchased')?.gems ? 0 + gemsSpent = @get('spent') ? 0 gemsEarned + gemsPurchased - gemsSpent - earnedHero: (heroOriginal) -> heroOriginal in (me.get('earned')?.heroes ? []) - earnedItem: (itemOriginal) -> itemOriginal in (me.get('earned')?.items ? []) - earnedLevel: (levelOriginal) -> levelOriginal in (me.get('earned')?.levels ? []) + heroes: -> (me.get('earned')?.heroes ? []).concat(me.get('purchased')?.heroes ? []) + items: -> (me.get('earned')?.items ? []).concat(me.get('purchased')?.items ? []) + levels: -> (me.get('earned')?.levels ? []).concat(me.get('purchased')?.levels ? []) + ownsHero: (heroOriginal) -> heroOriginal in @heroes() + ownsItem: (itemOriginal) -> itemOriginal in @items() + ownsLevel: (levelOriginal) -> levelOriginal in @levels() getBranchingGroup: -> return @branchingGroup if @branchingGroup diff --git a/app/schemas/models/purchase.schema.coffee b/app/schemas/models/purchase.schema.coffee new file mode 100644 index 000000000..9808725b1 --- /dev/null +++ b/app/schemas/models/purchase.schema.coffee @@ -0,0 +1,21 @@ +c = require './../schemas' + +purchaseables = ['level', 'thang_type'] + +PurchaseSchema = c.object({title: 'Purchase', required: ['purchaser', 'recipient', 'purchased']}, { + purchaser: c.objectId(links: [ + {rel: 'extra', href: '/db/user/{($)}'} + ]) # in case of gifts + recipient: c.objectId(links: [ + {rel: 'extra', href: '/db/user/{($)}'} + ]) + purchased: c.object({title: 'Target', required: ['collection', 'original']}, { + collection: {enum: purchaseables} + original: c.objectId(title: 'Target Original') + }) + created: c.date({title: 'Created', readOnly: true}) +}) + +c.extendBasicProperties(PurchaseSchema, 'patch') + +module.exports = PurchaseSchema \ No newline at end of file diff --git a/app/schemas/models/user.coffee b/app/schemas/models/user.coffee index 11e9c9716..aff178469 100644 --- a/app/schemas/models/user.coffee +++ b/app/schemas/models/user.coffee @@ -268,7 +268,8 @@ _.extend UserSchema.properties, thangTypeMiscPatches: c.int() earned: c.RewardSchema 'earned by achievements' - purchased: c.RewardSchema 'purchased with gems' + purchased: c.RewardSchema 'purchased with gems or money' + spent: {type: 'number'} c.extendBasicProperties UserSchema, 'user' diff --git a/app/styles/achievements.sass b/app/styles/achievements.sass index 4b3c95e3b..3ad9b4dc2 100644 --- a/app/styles/achievements.sass +++ b/app/styles/achievements.sass @@ -126,7 +126,7 @@ $user-achievements-scale: 0.8 padding: $overall-scale * 24px $overall-scale * 30px $overall-scale * 20px $overall-scale * 60px .achievement-title - font-family: Bangers + font-family: Open Sans Condensed font-size: $overall-scale * 28px padding-left: $overall-scale * -50px diff --git a/app/styles/base.sass b/app/styles/base.sass index 10bef6efb..e91fe640b 100644 --- a/app/styles/base.sass +++ b/app/styles/base.sass @@ -64,7 +64,7 @@ h1 h2 h3 h4 margin: 10px 10px 0px .footer-link-text a - font-family: 'Bangers', cursive + font-family: 'Open Sans Condensed', cursive font-weight: normal font-size: 25px letter-spacing: 1px diff --git a/app/styles/bootstrap/_bootswatch.scss b/app/styles/bootstrap/_bootswatch.scss index 263da36b2..dcb68f657 100644 --- a/app/styles/bootstrap/_bootswatch.scss +++ b/app/styles/bootstrap/_bootswatch.scss @@ -6,7 +6,6 @@ // TYPOGRAPHY // ----------------------------------------------------- -@import url(//fonts.googleapis.com/css?family=Bangers); @import url(//fonts.googleapis.com/css?family=Open+Sans+Condensed:700&subset=latin,latin-ext,cyrillic-ext,greek-ext,greek,vietnamese,cyrillic); // SCAFFOLDING diff --git a/app/styles/bootstrap/_variables.scss b/app/styles/bootstrap/_variables.scss index e35c82986..edb181e32 100644 --- a/app/styles/bootstrap/_variables.scss +++ b/app/styles/bootstrap/_variables.scss @@ -75,8 +75,8 @@ $font-size-h6: ceil($font-size-base * 0.85) !default; // ~12px $line-height-base: 1.428571429 !default; // 20/14 $line-height-computed: floor($font-size-base * $line-height-base) !default; // ~20px -$headings-font-family: 'Bangers', cursive; // empty to use BS default, $baseFontFamily; -$headings-font-weight: 500 !default; +$headings-font-family: 'Open Sans Condensed', cursive; // empty to use BS default, $baseFontFamily; +$headings-font-weight: 700 !default; $headings-line-height: 1.1 !default; $headings-color: #317EAC; diff --git a/app/styles/common/top_nav.sass b/app/styles/common/top_nav.sass index f659ee897..af657327a 100644 --- a/app/styles/common/top_nav.sass +++ b/app/styles/common/top_nav.sass @@ -74,7 +74,7 @@ a.disabled width: 280px padding: 0px border-radius: 0px - font-family: Bangers + font-family: Open Sans Condensed > .user-dropdown-header position: relative diff --git a/app/styles/home.sass b/app/styles/home.sass index e660b2af3..f1d9d0982 100644 --- a/app/styles/home.sass +++ b/app/styles/home.sass @@ -41,7 +41,7 @@ bottom: -25px color: $yellow font-size: 90px - font-family: Bangers + font-family: Open Sans Condensed @include transition(color .25s ease-in-out) &:hover div, &.hovered div @@ -79,7 +79,7 @@ bottom: -15px color: $yellow font-size: 50px - font-family: Bangers + font-family: Open Sans Condensed @include transition(color .10s linear) h1 diff --git a/app/styles/play/ladder_home.sass b/app/styles/play/ladder_home.sass index 48678ad59..b17a4ea50 100644 --- a/app/styles/play/ladder_home.sass +++ b/app/styles/play/ladder_home.sass @@ -21,7 +21,7 @@ .overlay-text color: $yellow - font-family: Bangers + font-family: Open Sans Condensed @include transition(color .10s linear) .level-difficulty diff --git a/app/styles/play/level/modal/victory.sass b/app/styles/play/level/modal/victory.sass index b393e2d76..9a343b3ce 100644 --- a/app/styles/play/level/modal/victory.sass +++ b/app/styles/play/level/modal/victory.sass @@ -55,4 +55,4 @@ body.ipad #level-victory-modal .modal-body font-size: 30px - font-family: Bangers + font-family: Open Sans Condensed diff --git a/app/styles/play/modal/play-items-modal.sass b/app/styles/play/modal/play-items-modal.sass index 71c03ee4e..c3c7f7ae3 100644 --- a/app/styles/play/modal/play-items-modal.sass +++ b/app/styles/play/modal/play-items-modal.sass @@ -1,8 +1,347 @@ +@import "app/styles/mixins" + #play-items-modal - .item-view - width: 420px - float: left - background-color: white - border-radius: 6px - margin: 5px + .big-font + text-transform: uppercase + font-family: "Open Sans Condensed" + font-weight: bold + + .one-line + white-space: nowrap + overflow: hidden + text-overflow: ellipsis + + //- Clear modal defaults + .modal-dialog + padding: 0 + + + //- Background + #play-items-modal-bg + position: absolute + top: -69px + left: -8px + + + //- Header + + h1 + position: absolute + left: 200px + top: 25px + color: rgb(254,195,70) + font-size: 38px + text-shadow: black 4px 4px 0, black -4px -4px 0, black 4px -4px 0, black -4px 4px 0, black 4px 0px 0, black 0px -4px 0, black -4px 0px 0, black 0px 4px 0 + margin: 0 + + + //- Gems count + + #gems-count-container + position: absolute + left: 425px + top: 10px + width: 160px + height: 66px + + #gems-count + position: absolute + left: 75px + top: 17px + font-size: 25px + color: rgb(1,64,91) + + + //- Close modal button + + #close-modal + position: absolute + left: 602px + top: 23px + width: 60px + height: 60px + color: white + text-align: center + font-size: 30px + padding-top: 7px + cursor: pointer + + &:hover + color: yellow + + + //- Nav bar + + .nav + position: absolute + top: 125px + left: -31px + width: 178px + + li + background: url(/images/pages/play/modal/play-items-modal-tab.png) + padding: 5px + margin: -5px 0 + height: 80px + padding: 0 + + a + font-size: 18px + line-height: 50px + background: none + color: rgb(195,153,124) + font-weight: bold + padding: 10px 7px + + //img + + + li.active + background: url(/images/pages/play/modal/play-items-modal-selected-tab.png) + width: 197px + + a + color: white + + + //- Item List + + .tab-content + position: absolute + top: 116px + left: 148px + width: 669px + height: 507px + overflow: hidden + + .tab-pane + height: 100% + + .nano-content + padding: 26px 51px 26px 26px + + + //- Item box + + .item + cursor: pointer + width: 187px + padding: 10px + height: 195px + float: left + background: url(/images/pages/play/modal/play-items-modal-item-background.png) + margin: 4px + text-align: center + position: relative + + strong + position: absolute + top: 7px + padding: 2px + left: 0 + right: 0 + font-size: 18px + z-index: 2 + line-height: 18px + color: rgb(22,16,5) + + img + width: 90px + height: 90px + &.item-img + top: 45px + &.item-shadow + top: 55px + &.item-silhouette + top: 25px + width: 110px + height: 110px + + .glyphicon-lock + font-size: 60px + position: absolute + top: 50px + color: rgb(149,141,123) + z-index: 1 + left: 0 + right: 0 + margin-left: auto + margin-right: auto + + &.bolder + font-weight: bolder + color: rgb(211,200,175) + + .unlock-button + right: 1px + bottom: 0 + width: 93px + height: 41px + font-size: 16px + + .cost + position: absolute + height: 41px + left: 0 + bottom: 0 + width: 95px + line-height: 38px + font-size: 16px + color: rgb(22,61,73) + font-weight: bold + + img + width: 22px + height: 22px + margin-right: 8px + position: relative + top: -2px + + .owned, .locked + position: absolute + left: 0 + right: 0 + bottom: 0 + height: 41px + color: rgb(22,61,73) + line-height: 38px + font-size: 16px + + &.selected + background: url(/images/pages/play/modal/play-items-modal-selected-item-background.png) + + + //- Item list scrollbar + + .nano-pane + width: 16px + background: black + border: 3px solid rgb(97,76,58) + + .nano-slider + background: rgb(244,170,66) + border: 3px solid black + border-radius: 10px + margin-left: -3px + margin-right: -3px + + // color: red + + + //- Item details + + #item-title + position: absolute + width: 228px + height: 50px + left: 910px + top: 60px + z-index: 2 + + h2 + font-size: 20px + margin: 12px 20px + text-align: center + color: rgb(53,40,25) + + #item-details-body + position: absolute + left: 860px + top: 126px + width: 330px + height: 453px + //background: rgba(100,100,100,0.5) + overflow: scroll + + #item-container + height: 163px + width: 100% + + .item-img, .item-shadow + width: 130px + height: 130px + + .item-img + top: 15px + + .item-shadow + top: 25px + + img.hr + width: 80% + margin: 0 10% -3px + + &.faded + opacity: 0.4 + + .stat-row + height: 24px + position: relative + font-size: 20px + font-weight: bold + + .stat-label + position: absolute + left: 54px + color: rgb(93,73,52) + + .stat + position: absolute + left: 150px + color: rgb(42,38,28) + + #skills + margin: 25px + + h3 + color: rgb(41,35,25) + + strong + color: rgb(50,50,30) + + #selected-item-unlock-button + left: 856px + top: 594px + width: 337px + height: 41px + font-size: 16px + + + //- Item icons w/shadows (both in list and details areas) + + .item-img, .item-shadow, .item-silhouette + position: absolute + margin-left: auto + margin-right: auto + left: 0 + right: 0 + bottom: 0 + + .item-img + z-index: 1 + + .item-shadow + left: 5px + @include filter(contrast(0%) brightness(0%)) + opacity: 0.2 + + .item-silhouette + @include filter(contrast(0%) brightness(0%)) + opacity: 0.3 + + + //- Unlock buttons (both in list and details areas) + + .unlock-button + position: absolute + border: 3px solid rgb(7,65,83) + background: rgb(0,119,168) + color: white + font-size: 16px + border-radius: 0 + + &:disabled + background: rgb(72, 106, 113) + opacity: 1 + color: rgba(255,255,255, 0.4) diff --git a/app/templates/play/modal/item-details-view.jade b/app/templates/play/modal/item-details-view.jade new file mode 100644 index 000000000..dbe18d04a --- /dev/null +++ b/app/templates/play/modal/item-details-view.jade @@ -0,0 +1,29 @@ + +#item-title + h2.one-line.big-font= item ? item.name : '' + +#item-details-body + if item + #item-container + img.item-img(src=item.getPortraitURL()) + img.item-shadow(src=item.getPortraitURL()) + + img.hr(src="/images/pages/play/modal/play-items-modal-hr.png") + + for stat in stats + div.stat-row.big-font + div.stat-label= stat.name + div.stat= stat.display + img.hr(src="/images/pages/play/modal/play-items-modal-hr.png" class=stat.isLast ? "" : "faded") + + if props.length + #skills + h3.big-font(data-i18n="play.skills-granted") + for prop in props + p + strong.big-font= prop.name + span.spr : + span!= prop.description + +if item && !item.owned + button#selected-item-unlock-button.btn.big-font.unlock-button(disabled=!item.affordable, data-item-id=item.id, data-i18n="play.unlock") Unlock \ No newline at end of file diff --git a/app/templates/play/modal/play-items-modal.jade b/app/templates/play/modal/play-items-modal.jade index 172c500bc..266481d7e 100644 --- a/app/templates/play/modal/play-items-modal.jade +++ b/app/templates/play/modal/play-items-modal.jade @@ -1,17 +1,48 @@ -extends /templates/modal/modal_base - -block modal-header-content - h3(data-i18n="play.items") Items - -block modal-body-content - ul.nav.nav-tabs - for slotGroup, index in slotGroupsArray - li(class=index ? "" : "active") - a(href="#slot-group-" + slotGroup, data-toggle="tab") - span= slotGroupsNames[index] - .tab-content - for slotGroup, index in slotGroupsArray - .tab-pane(id="slot-group-" + slotGroup, class=index ? "" : "active") - h3 buy some #{slotGroupsNames[index]} yo: - for item in slotGroups[slotGroup] - .replace-me(data-item-id=item.id) +.modal-dialog + .modal-content + img(src="/images/pages/play/modal/play-items-modal-background.png")#play-items-modal-bg + + h1.big-font(data-i18n="play.items") + + div#gems-count-container + span#gems-count.big-font= gems + + div#close-modal + span.glyphicon.glyphicon-remove + + ul.nav.nav-pills.nav-stacked + for category, index in itemCategories + li(class=index ? "" : "active") + a.one-line(href="#item-category-" + category, data-toggle="tab") + img.tab-icon(src="/images/pages/play/modal/play-items-modal-tab-icon-"+category+".png") + span.big-font= itemCategoryNames[index] + + + .tab-content + for category, index in itemCategories + .tab-pane(id="item-category-" + category, class=index ? "" : "active") + .nano + .nano-content + for item in itemCategoryCollections[category].models + .item(data-item-id=item.id) + if item.silhouetted && !item.owned + span.glyphicon.glyphicon-lock.bolder + span.glyphicon.glyphicon-lock + img.item-silhouette(src=item.getPortraitURL()) + else + strong.big-font= item.name + img.item-img(src=item.getPortraitURL()) + img.item-shadow(src=item.getPortraitURL()) + + if item.owned + span.big-font.owned(data-i18n="play.owned") + else if item.silhouetted + span.big-font.locked(data-i18n="play.locked") + else + span.cost + img(src="/images/common/gem.png") + span.big-font= item.get('gems') + 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/views/HomeView.coffee b/app/views/HomeView.coffee index 0370212c3..e7582e8ce 100644 --- a/app/views/HomeView.coffee +++ b/app/views/HomeView.coffee @@ -3,6 +3,9 @@ template = require 'templates/home' WizardLank = require 'lib/surface/WizardLank' ThangType = require 'models/ThangType' Simulator = require 'lib/simulator/Simulator' + +PlayItemsModal = require 'views/play/modal/PlayItemsModal' # TEST + {me} = require '/lib/auth' module.exports = class HomeView extends RootView @@ -34,3 +37,8 @@ module.exports = class HomeView extends RootView e.stopImmediatePropagation() window.tracker?.trackEvent 'Homepage', Action: 'Play' window.open '/play', '_blank' + + # TEST + afterInsert: -> + super() + @openModalView new PlayItemsModal() diff --git a/app/views/game-menu/ChooseHeroView.coffee b/app/views/game-menu/ChooseHeroView.coffee index 217869dd1..b322d3ed8 100644 --- a/app/views/game-menu/ChooseHeroView.coffee +++ b/app/views/game-menu/ChooseHeroView.coffee @@ -36,7 +36,7 @@ module.exports = class ChooseHeroView extends CocoView getRenderData: (context={}) -> context = super(context) context.heroes = @heroes.models - hero.locked = temporaryHeroInfo[hero.get('slug')].status is 'Locked' and not me.earnedHero hero.get('original') for hero in context.heroes + hero.locked = temporaryHeroInfo[hero.get('slug')].status is 'Locked' and not me.ownsHero hero.get('original') for hero in context.heroes context.level = @options.level context.codeLanguages = [ {id: 'python', name: 'Python (Default)'} @@ -79,7 +79,7 @@ module.exports = class ChooseHeroView extends CocoView size = 100 - (50 / 3) * distance $(@).css width: size, height: size, top: -(100 - size) / 2 heroInfo = temporaryHeroInfo[hero.get('slug')] - locked = heroInfo.status is 'Locked' and not me.earnedHero ThangType.heroes[hero.get('slug')] + locked = heroInfo.status is 'Locked' and not me.ownsHero ThangType.heroes[hero.get('slug')] hero = @loadHero hero, heroIndex @preloadHero heroIndex + 1 @preloadHero heroIndex - 1 diff --git a/app/views/kinds/CocoView.coffee b/app/views/kinds/CocoView.coffee index 115614be1..5d62f4ba3 100644 --- a/app/views/kinds/CocoView.coffee +++ b/app/views/kinds/CocoView.coffee @@ -93,6 +93,13 @@ module.exports = class CocoView extends Backbone.View # View Rendering + renderSelectors: (selectors...) -> + newTemplate = $(@template(@getRenderData())) + for selector in selectors + @$el.find(selector).replaceWith(newTemplate.find(selector)) + @delegateEvents() + @$el.i18n() + render: -> return @ unless me view.destroy() for id, view of @subviews diff --git a/app/views/play/WorldMapView.coffee b/app/views/play/WorldMapView.coffee index 4a9198c1c..d1751f5db 100644 --- a/app/views/play/WorldMapView.coffee +++ b/app/views/play/WorldMapView.coffee @@ -83,7 +83,7 @@ module.exports = class WorldMapView extends RootView for level, index in context.campaign.levels level.x ?= 10 + 80 * Math.random() level.y ?= 10 + 80 * Math.random() - level.locked = index > 0 and not me.earnedLevel level.original + level.locked = index > 0 and not me.ownsLevel level.original window.levelUnlocksNotWorking = true if level.locked and level.id is @nextLevel # Temporary level.locked = false if window.levelUnlocksNotWorking # Temporary; also possible in HeroVictoryModal level.color = 'rgb(255, 80, 60)' diff --git a/app/views/play/modal/PlayItemsModal.coffee b/app/views/play/modal/PlayItemsModal.coffee index 3fc55c74c..463d08aab 100644 --- a/app/views/play/modal/PlayItemsModal.coffee +++ b/app/views/play/modal/PlayItemsModal.coffee @@ -1,65 +1,225 @@ ModalView = require 'views/kinds/ModalView' +CocoView = require 'views/kinds/CocoView' + template = require 'templates/play/modal/play-items-modal' +itemDetailsTemplate = require 'templates/play/modal/item-details-view' + CocoCollection = require 'collections/CocoCollection' ThangType = require 'models/ThangType' -ItemView = require 'views/game-menu/ItemView' +LevelComponent = require 'models/LevelComponent' +Purchase = require 'models/Purchase' + +utils = require 'lib/utils' + +PAGE_SIZE = 200 + +slotToCategory = { + 'right-hand': 'primary' + + 'left-hand': 'secondary' + + 'head': 'armor' + 'torso': 'armor' + 'gloves': 'armor' + 'feet': 'armor' + + 'eyes': 'accessories' + 'neck': 'accessories' + 'wrists': 'accessories' + 'left-ring': 'accessories' + 'right-ring': 'accessories' + 'waist': 'accessories' + + 'pet': 'misc' + 'minion': 'misc' + 'flag': 'misc' + 'misc-0': 'misc' + 'misc-1': 'misc' + + 'programming-book': 'books' +} module.exports = class PlayItemsModal extends ModalView className: 'modal fade play-modal' template: template - modalWidthPercent: 90 id: 'play-items-modal' - #instant: true - slotGroups: - armor: ['torso', 'head', 'gloves', 'feet'] - hands: ['right-hand', 'left-hand'] - accessories: ['eyes', 'neck', 'left-ring', 'right-ring', 'waist'] - minions: ['minion', 'pet'] - misc: ['programming-book', 'flag', 'misc-0', 'misc-1'] - #events: - # 'change input.select': 'onSelectionChanged' + events: + 'click .item': 'onItemClicked' + 'shown.bs.tab': 'onTabClicked' + 'click .unlock-button': 'onUnlockButtonClicked' + 'click #close-modal': 'hide' constructor: (options) -> super options - @items = new CocoCollection([], {model: ThangType}) - @items.url = '/db/thang.type?view=items&project=name,description,components,original,rasterIcon' - @supermodel.loadCollection(@items, 'items') + me.set('spent', 0) + @items = new Backbone.Collection() + @itemCategoryCollections = {} + + project = [ + 'name' + 'components.config' + 'components.original' + 'slug' + 'original' + 'rasterIcon' + 'gems' + 'i18n' + ] + + itemFetcher = new CocoCollection([], { url: '/db/thang.type?view=items', project: project, model: ThangType }) + itemFetcher.skip = 0 + itemFetcher.fetch({data: {skip: 0, limit: PAGE_SIZE}}) + @listenTo itemFetcher, 'sync', @onItemsFetched + @supermodel.loadCollection(itemFetcher, 'items') + @idToItem = {} - groupItems: -> - groups = {} - for item in @items.models - itemSlots = item.getAllowedSlots() - for group, groupSlots of @slotGroups - if _.find itemSlots, ((slot) -> slot in groupSlots) - groups[group] ?= [] - groups[group].push item - groups + onItemsFetched: (itemFetcher) -> + gemsOwned = me.gems() + needMore = itemFetcher.models.length is PAGE_SIZE + for model in itemFetcher.models + continue unless cost = model.get('gems') + category = slotToCategory[model.getAllowedSlots()[0]] or 'misc' + @itemCategoryCollections[category] ?= new Backbone.Collection() + collection = @itemCategoryCollections[category] + collection.comparator = 'gems' + collection.add(model) + model.name = utils.i18n model.attributes, 'name' + model.affordable = cost <= gemsOwned + model.owned = me.ownsItem model.get('original') + model.silhouetted = model.isSilhouettedItem() + @idToItem[model.id] = model + if needMore + itemFetcher.skip += PAGE_SIZE + itemFetcher.fetch({data: {skip: itemFetcher.skip, limit: PAGE_SIZE}}) + getRenderData: (context={}) -> context = super(context) - context.slotGroups = @groupItems() - context.slotGroupsArray = _.keys context.slotGroups - context.slotGroupsNames = ($.i18n.t "items.#{slotGroup}" for slotGroup in context.slotGroupsArray) + context.itemCategoryCollections = @itemCategoryCollections + context.itemCategories = _.keys @itemCategoryCollections + context.itemCategoryNames = ($.i18n.t "items.#{category}" for category in context.itemCategories) + context.gems = me.gems() context afterRender: -> super() return unless @supermodel.finished() Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'game-menu-open', volume: 1 - @addItemViews() + @$el.find('.modal-dialog').css({width: "1230px", height: "660px", background: 'none'}) + @$el.find('.background-wrapper').css({'background', 'none'}) + @$el.find('.nano:visible').nanoScroller({alwaysVisible: true}) + @itemDetailsView = new ItemDetailsView() + @insertSubView(@itemDetailsView) onHidden: -> super() Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'game-menu-close', volume: 1 - addItemViews: -> - keys = (item.id for item in @items.models) - itemMap = _.zipObject keys, @items.models - for itemStub in @$el.find('.replace-me') - itemID = $(itemStub).data('item-id') - item = itemMap[itemID] - itemView = new ItemView({item: item, includes: {name: true, stats: true, props: true}}) - itemView.render() - $(itemStub).replaceWith(itemView.$el) - @registerSubView(itemView) + + #- Click events + + onItemClicked: (e) -> + return if $(e.target).closest('.unlock-button').length + itemEl = $(e.target).closest('.item') + wasSelected = itemEl.hasClass('selected') + @$el.find('.item.selected').removeClass('selected') + if wasSelected + item = null + else + item = @idToItem[itemEl.data('item-id')] + if item.silhouetted + item = null + else + itemEl.addClass('selected') unless wasSelected + @itemDetailsView.setItem(item) + + onTabClicked: (e) -> + $($(e.target).attr('href')).find('.nano').nanoScroller({alwaysVisible: true}) + + onUnlockButtonClicked: (e) -> + button = $(e.target) + if button.hasClass('confirm') + item = @idToItem[$(e.target).data('item-id')] + purchase = Purchase.makeFor(item) + purchase.save() + + #- set local changes to mimic what should happen on the server... + purchased = me.get('purchased') ? {} + purchased.items ?= [] + purchased.items.push(item.get('original')) + item.owned = true + me.set('purchased', purchased) + me.set('spent', (me.get('spent') ? 0) + item.get('gems')) + + #- ...then rerender key bits + @renderSelectors(".item[data-item-id='#{item.id}']", "#gems-count") + @itemDetailsView.render() + else + button.addClass('confirm').text($.i18n.t('play.confirm')) + @$el.one 'click', (e) -> + button.removeClass('confirm').text($.i18n.t('play.unlock')) if e.target isnt button[0] + +class ItemDetailsView extends CocoView + id: "item-details-view" + template: itemDetailsTemplate + + constructor: -> + super(arguments...) + @propDocs = {} + + setItem: (@item) -> + @render() + + if @item + stats = @item.getFrontFacingStats() + props = (p for p in stats.props when not @propDocs[p]) + return if props.length is 0 + + docs = new CocoCollection([], { + url: '/db/level.component?view=prop-doc-lookup' + model: LevelComponent + project: [ + 'propertyDocumentation.name' + 'propertyDocumentation.description' + 'propertyDocumentation.i18n' + ] + }) + + docs.fetch({ data: { + componentOriginals: [c.original for c in @item.get('components')].join(',') + propertyNames: props.join(',') + }}) + @listenToOnce docs, 'sync', @onDocsLoaded + + onDocsLoaded: (levelComponents) -> + for component in levelComponents.models + for propDoc in component.get('propertyDocumentation') + @propDocs[propDoc.name] = propDoc + @render() + + getRenderData: -> + c = super() + c.item = @item + if @item + stats = @item.getFrontFacingStats() + c.stats = _.values(stats.stats) + _.last(c.stats).isLast = true if c.stats.length + c.props = [] + progLang = (me.get('aceConfig') ? {}).language or 'python' + for prop in stats.props + description = utils.i18n @propDocs[prop] ? {}, 'description' + + if _.isObject description + description = description[progLang] or _.values(description)[0] + if _.isString description + description = description.replace(/#{spriteName}/g, 'hero') + if fact = stats.stats.shieldDefenseFactor + description = description.replace(/#{shieldDefensePercent}%/g, fact.display) + description = $(marked(description)).html() + + c.props.push { + name: prop + description: description or '...' + } + c \ No newline at end of file diff --git a/server/commons/mapping.coffee b/server/commons/mapping.coffee index 08a65f319..2afb265d9 100644 --- a/server/commons/mapping.coffee +++ b/server/commons/mapping.coffee @@ -6,6 +6,7 @@ module.exports.handlers = 'level_session': 'levels/sessions/level_session_handler' 'level_system': 'levels/systems/level_system_handler' 'patch': 'patches/patch_handler' + 'purchase': 'purchases/purchase_handler' 'thang_type': 'levels/thangs/thang_type_handler' 'user': 'users/user_handler' 'user_code_problem': 'user_code_problems/user_code_problem_handler' diff --git a/server/levels/components/level_component_handler.coffee b/server/levels/components/level_component_handler.coffee index 41207680a..d35d5c5b7 100644 --- a/server/levels/components/level_component_handler.coffee +++ b/server/levels/components/level_component_handler.coffee @@ -1,5 +1,6 @@ LevelComponent = require './LevelComponent' Handler = require '../../commons/Handler' +mongoose = require 'mongoose' LevelComponentHandler = class LevelComponentHandler extends Handler modelClass: LevelComponent @@ -22,4 +23,30 @@ LevelComponentHandler = class LevelComponentHandler extends Handler props.push('official') if req.user?.isAdmin() props + get: (req, res) -> + if req.query.view is 'prop-doc-lookup' + projection = {} + if req.query.project + projection[field] = 1 for field in req.query.project.split(',') + + query = slug: {$exists: true} + + try + components = req.query.componentOriginals.split(',') + components = (mongoose.Types.ObjectId(c) for c in components) + properties = req.query.propertyNames.split(',') + catch e + return @sendBadInputError(res, 'Could not parse componentOriginals or propertyNames.') + + query['original'] = {$in: components} + query['propertyDocumentation.name'] = {$in: properties} + + q = LevelComponent.find(query, projection) + q.exec (err, documents) => + return @sendDatabaseError(res, err) if err + documents = (@formatEntity(req, doc) for doc in documents) + @sendSuccess(res, documents) + else + super(arguments...) + module.exports = new LevelComponentHandler() diff --git a/server/purchases/Purchase.coffee b/server/purchases/Purchase.coffee new file mode 100644 index 000000000..b1f34459a --- /dev/null +++ b/server/purchases/Purchase.coffee @@ -0,0 +1,9 @@ +mongoose = require('mongoose') +deltas = require '../../app/lib/deltas' +log = require 'winston' +{handlers} = require '../commons/mapping' + +PurchaseSchema = new mongoose.Schema({status: String}, {strict: false}) +PurchaseSchema.index({recipient: 1, 'purchase.original': 1}, {unique: true, name: 'unique purchase'}) + +module.exports = mongoose.model('purchase', PurchaseSchema) diff --git a/server/purchases/purchase_handler.coffee b/server/purchases/purchase_handler.coffee new file mode 100644 index 000000000..72a943edf --- /dev/null +++ b/server/purchases/purchase_handler.coffee @@ -0,0 +1,86 @@ +Purchase = require './Purchase' +User = require '../users/User' +Handler = require '../commons/Handler' +{handlers} = require '../commons/mapping' +mongoose = require 'mongoose' +log = require 'winston' +sendwithus = require '../sendwithus' +hipchat = require '../hipchat' + +PurchaseHandler = class PurchaseHandler extends Handler + modelClass: Purchase + editableProperties: [] + postEditableProperties: ['purchased'] + jsonSchema: require '../../app/schemas/models/purchase.schema' + + makeNewInstance: (req) -> + purchase = super(req) + purchase.set 'purchaser', req.user._id + purchase.set 'recipient', req.user._id + purchase.set 'created', new Date().toISOString() + purchase + + post: (req, res) -> + purchased = req.body.purchased + purchaser = req.user._id + purchasedOriginal = purchased?.original + + Handler = require '../commons/Handler' + return @sendBadInputError(res) if not Handler.isID(purchasedOriginal) + + collection = purchased?.collection + return @sendBadInputError(res) if not collection in @jsonSchema.properties.purchased.properties.collection.enum + + handler = require('../' + handlers[collection]) + criteria = { 'original': mongoose.Types.ObjectId(purchasedOriginal) } + sort = { 'version.major': -1, 'version.minor': -1 } + + handler.modelClass.findOne(criteria).sort(sort).exec (err, purchasedItem) => + gemsOwned = req.user.get('earned')?.gems or 0 + return @sendDatabaseError(res, err) if err + return @sendNotFoundError(res) unless purchasedItem + return @sendBadInputError(res, 'This cannot be purchased.') if not cost = purchasedItem.get('gems') + return @sendForbiddenError(res, 'Not enough gems.') if cost > req.user.get('gems') + req.purchasedItem = purchasedItem # for safekeeping + + criteria = { + 'purchased.original': purchased?.original + 'recipient': purchaser + } + Purchase.findOne criteria, (err, purchase) => + if purchase + @addPurchaseToUser(req, res) + return @sendSuccess(res, @formatEntity(req, purchase)) + + else + super(req, res) + + onPostSuccess: (req) -> + @addPurchaseToUser(req) + + addPurchaseToUser: (req) -> + user = req.user + purchased = user.get('purchased') or {} + purchased = _.clone purchased + item = req.purchasedItem + + group = switch item.get('kind') + when 'Item' then 'items' + when 'Hero' then 'heroes' + else 'levels' + + original = item.get('original') + purchased[group] ?= [] + unless original in purchased[group] + #- add the purchase to the list of purchases + purchased[group].push(original) + user.set('purchased', purchased) + + #- deduct the gems from the user + spent = hadSpent = user.get('spent') ? 0 + spent += item.get('gems') + user.set('spent', spent) + + user.save() + +module.exports = new PurchaseHandler() diff --git a/server/users/User.coffee b/server/users/User.coffee index fa4ee0612..365b8176b 100644 --- a/server/users/User.coffee +++ b/server/users/User.coffee @@ -59,6 +59,12 @@ UserSchema.methods.setEmailSubscription = (newName, enabled) -> newSubs[newName].enabled = enabled @set('emails', newSubs) @newsSubsChanged = true if newName in mail.NEWS_GROUPS + +UserSchema.methods.gems = -> + gemsEarned = @get('earned')?.gems ? 0 + gemsPurchased = @get('purchased')?.gems ? 0 + gemsSpent = @get('spent') ? 0 + gemsEarned + gemsPurchased - gemsSpent UserSchema.methods.isEmailSubscriptionEnabled = (newName) -> emails = @get 'emails'