Implementation of Heald's design of the PlayItemsModal. Added purchasing logic to the server to support it. Refactored header font from Bangers to Open Sans Condensed.
After Width: | Height: | Size: 108 KiB |
BIN
app/assets/images/pages/play/modal/play-items-modal-hr.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 4.7 KiB |
After Width: | Height: | Size: 4 KiB |
After Width: | Height: | Size: 5.5 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 4.3 KiB |
BIN
app/assets/images/pages/play/modal/play-items-modal-tab.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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:
|
||||
|
|
16
app/models/Purchase.coffee
Normal file
|
@ -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
|
||||
}
|
||||
})
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
21
app/schemas/models/purchase.schema.coffee
Normal file
|
@ -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
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
|
||||
.overlay-text
|
||||
color: $yellow
|
||||
font-family: Bangers
|
||||
font-family: Open Sans Condensed
|
||||
@include transition(color .10s linear)
|
||||
|
||||
.level-difficulty
|
||||
|
|
|
@ -55,4 +55,4 @@ body.ipad #level-victory-modal
|
|||
|
||||
.modal-body
|
||||
font-size: 30px
|
||||
font-family: Bangers
|
||||
font-family: Open Sans Condensed
|
||||
|
|
|
@ -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)
|
||||
|
|
29
app/templates/play/modal/item-details-view.jade
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)'
|
||||
|
|
|
@ -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
|
|
@ -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'
|
||||
|
|
|
@ -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()
|
||||
|
|
9
server/purchases/Purchase.coffee
Normal file
|
@ -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)
|
86
server/purchases/purchase_handler.coffee
Normal file
|
@ -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()
|
|
@ -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'
|
||||
|
|