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: ->
|
makeCountdownText: ->
|
||||||
size = Math.ceil @camera.canvasHeight / 2
|
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.shadow = new createjs.Shadow '#000', Math.ceil(@camera.canvasHeight / 300), Math.ceil(@camera.canvasHeight / 300), Math.ceil(@camera.canvasHeight / 120)
|
||||||
text.textAlign = 'center'
|
text.textAlign = 'center'
|
||||||
text.textBaseline = 'middle'
|
text.textBaseline = 'middle'
|
||||||
|
|
|
@ -34,7 +34,7 @@ module.exports = class WaitingScreen extends CocoClass
|
||||||
|
|
||||||
makeWaitingText: ->
|
makeWaitingText: ->
|
||||||
size = Math.ceil @camera.canvasHeight / 8
|
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.shadow = new createjs.Shadow '#000', Math.ceil(@camera.canvasHeight / 300), Math.ceil(@camera.canvasHeight / 300), Math.ceil(@camera.canvasHeight / 120)
|
||||||
text.textAlign = 'center'
|
text.textAlign = 'center'
|
||||||
text.textBaseline = 'middle'
|
text.textBaseline = 'middle'
|
||||||
|
|
|
@ -51,6 +51,11 @@
|
||||||
players: "players" # Hover over a level on /play
|
players: "players" # Hover over a level on /play
|
||||||
hours_played: "hours played" # 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
|
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
|
heroes: "Heroes" # Tooltip on hero shop button from /play
|
||||||
achievements: "Achievements" # Tooltip on achievement list button from /play
|
achievements: "Achievements" # Tooltip on achievement list button from /play
|
||||||
account: "Account" # Tooltip on account button from /play
|
account: "Account" # Tooltip on account button from /play
|
||||||
|
@ -104,11 +109,12 @@
|
||||||
recovery_sent: "Recovery email sent."
|
recovery_sent: "Recovery email sent."
|
||||||
|
|
||||||
items:
|
items:
|
||||||
|
primary: "Primary"
|
||||||
|
secondary: "Secondary"
|
||||||
armor: "Armor"
|
armor: "Armor"
|
||||||
hands: "Hands"
|
|
||||||
accessories: "Accessories"
|
accessories: "Accessories"
|
||||||
minions: "Minions"
|
|
||||||
misc: "Misc"
|
misc: "Misc"
|
||||||
|
books: "Books"
|
||||||
|
|
||||||
common:
|
common:
|
||||||
loading: "Loading..."
|
loading: "Loading..."
|
||||||
|
@ -306,6 +312,9 @@
|
||||||
attack: "Damage" # Can also translate as "Attack"
|
attack: "Damage" # Can also translate as "Attack"
|
||||||
health: "Health"
|
health: "Health"
|
||||||
speed: "Speed"
|
speed: "Speed"
|
||||||
|
regeneration: "Regeneration"
|
||||||
|
range: "Range" # As in "attack or visual range"
|
||||||
|
blocks: "Blocks" # As in "this shield blocks this much damage"
|
||||||
skills: "Skills"
|
skills: "Skills"
|
||||||
|
|
||||||
save_load:
|
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'
|
SpriteBuilder = require 'lib/sprites/SpriteBuilder'
|
||||||
LevelComponent = require './LevelComponent'
|
LevelComponent = require './LevelComponent'
|
||||||
|
|
||||||
|
utils = require 'lib/utils'
|
||||||
|
|
||||||
buildQueue = []
|
buildQueue = []
|
||||||
|
|
||||||
module.exports = class ThangType extends CocoModel
|
module.exports = class ThangType extends CocoModel
|
||||||
|
@ -328,14 +330,30 @@ module.exports = class ThangType extends CocoModel
|
||||||
props: props, stats: stats
|
props: props, stats: stats
|
||||||
|
|
||||||
formatStatDisplay: (name, modifiers) ->
|
formatStatDisplay: (name, modifiers) ->
|
||||||
name = {maxHealth: 'Health', maxSpeed: 'Speed', healthReplenishRate: 'Regeneration'}[name] ? 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
|
name = _.string.humanize name
|
||||||
|
|
||||||
format = ''
|
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 ||= 's' if /cooldown$/i.test name
|
||||||
format ||= 'm/s' if /speed$/i.test name
|
format ||= 'm/s' if /speed$/i.test name
|
||||||
format ||= '/s' if /(regeneration| rate)$/i.test name
|
format ||= '/s' if /(regeneration| rate)$/i.test name
|
||||||
value = modifiers.setTo
|
value = modifiers.setTo
|
||||||
|
if /(blocks)$/i.test name
|
||||||
|
format ||= '%'
|
||||||
|
value = (value*100).toFixed(1)
|
||||||
value = value.join ', ' if _.isArray value
|
value = value.join ', ' if _.isArray value
|
||||||
display = []
|
display = []
|
||||||
display.push "#{value}#{format}" if value?
|
display.push "#{value}#{format}" if value?
|
||||||
|
|
|
@ -67,15 +67,16 @@ module.exports = class User extends CocoModel
|
||||||
|
|
||||||
gems: ->
|
gems: ->
|
||||||
gemsEarned = @get('earned')?.gems ? 0
|
gemsEarned = @get('earned')?.gems ? 0
|
||||||
purchased = @get('purchased') ? {}
|
gemsPurchased = @get('purchased')?.gems ? 0
|
||||||
gemsPurchased = purchased.gems ? 0
|
gemsSpent = @get('spent') ? 0
|
||||||
sum = (arr) -> arr?.reduce((a, b) -> a + b) ? 0
|
|
||||||
gemsSpent = sum(purchased.heroes) + sum(purchased.items) + sum(purchased.levels)
|
|
||||||
gemsEarned + gemsPurchased - gemsSpent
|
gemsEarned + gemsPurchased - gemsSpent
|
||||||
|
|
||||||
earnedHero: (heroOriginal) -> heroOriginal in (me.get('earned')?.heroes ? [])
|
heroes: -> (me.get('earned')?.heroes ? []).concat(me.get('purchased')?.heroes ? [])
|
||||||
earnedItem: (itemOriginal) -> itemOriginal in (me.get('earned')?.items ? [])
|
items: -> (me.get('earned')?.items ? []).concat(me.get('purchased')?.items ? [])
|
||||||
earnedLevel: (levelOriginal) -> levelOriginal in (me.get('earned')?.levels ? [])
|
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: ->
|
getBranchingGroup: ->
|
||||||
return @branchingGroup if @branchingGroup
|
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()
|
thangTypeMiscPatches: c.int()
|
||||||
|
|
||||||
earned: c.RewardSchema 'earned by achievements'
|
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'
|
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
|
padding: $overall-scale * 24px $overall-scale * 30px $overall-scale * 20px $overall-scale * 60px
|
||||||
|
|
||||||
.achievement-title
|
.achievement-title
|
||||||
font-family: Bangers
|
font-family: Open Sans Condensed
|
||||||
font-size: $overall-scale * 28px
|
font-size: $overall-scale * 28px
|
||||||
padding-left: $overall-scale * -50px
|
padding-left: $overall-scale * -50px
|
||||||
|
|
||||||
|
|
|
@ -64,7 +64,7 @@ h1 h2 h3 h4
|
||||||
margin: 10px 10px 0px
|
margin: 10px 10px 0px
|
||||||
|
|
||||||
.footer-link-text a
|
.footer-link-text a
|
||||||
font-family: 'Bangers', cursive
|
font-family: 'Open Sans Condensed', cursive
|
||||||
font-weight: normal
|
font-weight: normal
|
||||||
font-size: 25px
|
font-size: 25px
|
||||||
letter-spacing: 1px
|
letter-spacing: 1px
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
// TYPOGRAPHY
|
// 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);
|
@import url(//fonts.googleapis.com/css?family=Open+Sans+Condensed:700&subset=latin,latin-ext,cyrillic-ext,greek-ext,greek,vietnamese,cyrillic);
|
||||||
|
|
||||||
// SCAFFOLDING
|
// 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-base: 1.428571429 !default; // 20/14
|
||||||
$line-height-computed: floor($font-size-base * $line-height-base) !default; // ~20px
|
$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-family: 'Open Sans Condensed', cursive; // empty to use BS default, $baseFontFamily;
|
||||||
$headings-font-weight: 500 !default;
|
$headings-font-weight: 700 !default;
|
||||||
$headings-line-height: 1.1 !default;
|
$headings-line-height: 1.1 !default;
|
||||||
$headings-color: #317EAC;
|
$headings-color: #317EAC;
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,7 @@ a.disabled
|
||||||
width: 280px
|
width: 280px
|
||||||
padding: 0px
|
padding: 0px
|
||||||
border-radius: 0px
|
border-radius: 0px
|
||||||
font-family: Bangers
|
font-family: Open Sans Condensed
|
||||||
|
|
||||||
> .user-dropdown-header
|
> .user-dropdown-header
|
||||||
position: relative
|
position: relative
|
||||||
|
|
|
@ -41,7 +41,7 @@
|
||||||
bottom: -25px
|
bottom: -25px
|
||||||
color: $yellow
|
color: $yellow
|
||||||
font-size: 90px
|
font-size: 90px
|
||||||
font-family: Bangers
|
font-family: Open Sans Condensed
|
||||||
@include transition(color .25s ease-in-out)
|
@include transition(color .25s ease-in-out)
|
||||||
|
|
||||||
&:hover div, &.hovered div
|
&:hover div, &.hovered div
|
||||||
|
@ -79,7 +79,7 @@
|
||||||
bottom: -15px
|
bottom: -15px
|
||||||
color: $yellow
|
color: $yellow
|
||||||
font-size: 50px
|
font-size: 50px
|
||||||
font-family: Bangers
|
font-family: Open Sans Condensed
|
||||||
@include transition(color .10s linear)
|
@include transition(color .10s linear)
|
||||||
|
|
||||||
h1
|
h1
|
||||||
|
|
|
@ -21,7 +21,7 @@
|
||||||
|
|
||||||
.overlay-text
|
.overlay-text
|
||||||
color: $yellow
|
color: $yellow
|
||||||
font-family: Bangers
|
font-family: Open Sans Condensed
|
||||||
@include transition(color .10s linear)
|
@include transition(color .10s linear)
|
||||||
|
|
||||||
.level-difficulty
|
.level-difficulty
|
||||||
|
|
|
@ -55,4 +55,4 @@ body.ipad #level-victory-modal
|
||||||
|
|
||||||
.modal-body
|
.modal-body
|
||||||
font-size: 30px
|
font-size: 30px
|
||||||
font-family: Bangers
|
font-family: Open Sans Condensed
|
||||||
|
|
|
@ -1,8 +1,347 @@
|
||||||
#play-items-modal
|
@import "app/styles/mixins"
|
||||||
.item-view
|
|
||||||
width: 420px
|
|
||||||
float: left
|
|
||||||
background-color: white
|
|
||||||
border-radius: 6px
|
|
||||||
margin: 5px
|
|
||||||
|
|
||||||
|
#play-items-modal
|
||||||
|
|
||||||
|
.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
|
.modal-dialog
|
||||||
|
.modal-content
|
||||||
|
img(src="/images/pages/play/modal/play-items-modal-background.png")#play-items-modal-bg
|
||||||
|
|
||||||
block modal-header-content
|
h1.big-font(data-i18n="play.items")
|
||||||
h3(data-i18n="play.items") Items
|
|
||||||
|
|
||||||
block modal-body-content
|
div#gems-count-container
|
||||||
ul.nav.nav-tabs
|
span#gems-count.big-font= gems
|
||||||
for slotGroup, index in slotGroupsArray
|
|
||||||
|
div#close-modal
|
||||||
|
span.glyphicon.glyphicon-remove
|
||||||
|
|
||||||
|
ul.nav.nav-pills.nav-stacked
|
||||||
|
for category, index in itemCategories
|
||||||
li(class=index ? "" : "active")
|
li(class=index ? "" : "active")
|
||||||
a(href="#slot-group-" + slotGroup, data-toggle="tab")
|
a.one-line(href="#item-category-" + category, data-toggle="tab")
|
||||||
span= slotGroupsNames[index]
|
img.tab-icon(src="/images/pages/play/modal/play-items-modal-tab-icon-"+category+".png")
|
||||||
|
span.big-font= itemCategoryNames[index]
|
||||||
|
|
||||||
|
|
||||||
.tab-content
|
.tab-content
|
||||||
for slotGroup, index in slotGroupsArray
|
for category, index in itemCategories
|
||||||
.tab-pane(id="slot-group-" + slotGroup, class=index ? "" : "active")
|
.tab-pane(id="item-category-" + category, class=index ? "" : "active")
|
||||||
h3 buy some #{slotGroupsNames[index]} yo:
|
.nano
|
||||||
for item in slotGroups[slotGroup]
|
.nano-content
|
||||||
.replace-me(data-item-id=item.id)
|
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'
|
WizardLank = require 'lib/surface/WizardLank'
|
||||||
ThangType = require 'models/ThangType'
|
ThangType = require 'models/ThangType'
|
||||||
Simulator = require 'lib/simulator/Simulator'
|
Simulator = require 'lib/simulator/Simulator'
|
||||||
|
|
||||||
|
PlayItemsModal = require 'views/play/modal/PlayItemsModal' # TEST
|
||||||
|
|
||||||
{me} = require '/lib/auth'
|
{me} = require '/lib/auth'
|
||||||
|
|
||||||
module.exports = class HomeView extends RootView
|
module.exports = class HomeView extends RootView
|
||||||
|
@ -34,3 +37,8 @@ module.exports = class HomeView extends RootView
|
||||||
e.stopImmediatePropagation()
|
e.stopImmediatePropagation()
|
||||||
window.tracker?.trackEvent 'Homepage', Action: 'Play'
|
window.tracker?.trackEvent 'Homepage', Action: 'Play'
|
||||||
window.open '/play', '_blank'
|
window.open '/play', '_blank'
|
||||||
|
|
||||||
|
# TEST
|
||||||
|
afterInsert: ->
|
||||||
|
super()
|
||||||
|
@openModalView new PlayItemsModal()
|
||||||
|
|
|
@ -36,7 +36,7 @@ module.exports = class ChooseHeroView extends CocoView
|
||||||
getRenderData: (context={}) ->
|
getRenderData: (context={}) ->
|
||||||
context = super(context)
|
context = super(context)
|
||||||
context.heroes = @heroes.models
|
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.level = @options.level
|
||||||
context.codeLanguages = [
|
context.codeLanguages = [
|
||||||
{id: 'python', name: 'Python (Default)'}
|
{id: 'python', name: 'Python (Default)'}
|
||||||
|
@ -79,7 +79,7 @@ module.exports = class ChooseHeroView extends CocoView
|
||||||
size = 100 - (50 / 3) * distance
|
size = 100 - (50 / 3) * distance
|
||||||
$(@).css width: size, height: size, top: -(100 - size) / 2
|
$(@).css width: size, height: size, top: -(100 - size) / 2
|
||||||
heroInfo = temporaryHeroInfo[hero.get('slug')]
|
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
|
hero = @loadHero hero, heroIndex
|
||||||
@preloadHero heroIndex + 1
|
@preloadHero heroIndex + 1
|
||||||
@preloadHero heroIndex - 1
|
@preloadHero heroIndex - 1
|
||||||
|
|
|
@ -93,6 +93,13 @@ module.exports = class CocoView extends Backbone.View
|
||||||
|
|
||||||
# View Rendering
|
# View Rendering
|
||||||
|
|
||||||
|
renderSelectors: (selectors...) ->
|
||||||
|
newTemplate = $(@template(@getRenderData()))
|
||||||
|
for selector in selectors
|
||||||
|
@$el.find(selector).replaceWith(newTemplate.find(selector))
|
||||||
|
@delegateEvents()
|
||||||
|
@$el.i18n()
|
||||||
|
|
||||||
render: ->
|
render: ->
|
||||||
return @ unless me
|
return @ unless me
|
||||||
view.destroy() for id, view of @subviews
|
view.destroy() for id, view of @subviews
|
||||||
|
|
|
@ -83,7 +83,7 @@ module.exports = class WorldMapView extends RootView
|
||||||
for level, index in context.campaign.levels
|
for level, index in context.campaign.levels
|
||||||
level.x ?= 10 + 80 * Math.random()
|
level.x ?= 10 + 80 * Math.random()
|
||||||
level.y ?= 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
|
window.levelUnlocksNotWorking = true if level.locked and level.id is @nextLevel # Temporary
|
||||||
level.locked = false if window.levelUnlocksNotWorking # Temporary; also possible in HeroVictoryModal
|
level.locked = false if window.levelUnlocksNotWorking # Temporary; also possible in HeroVictoryModal
|
||||||
level.color = 'rgb(255, 80, 60)'
|
level.color = 'rgb(255, 80, 60)'
|
||||||
|
|
|
@ -1,65 +1,225 @@
|
||||||
ModalView = require 'views/kinds/ModalView'
|
ModalView = require 'views/kinds/ModalView'
|
||||||
|
CocoView = require 'views/kinds/CocoView'
|
||||||
|
|
||||||
template = require 'templates/play/modal/play-items-modal'
|
template = require 'templates/play/modal/play-items-modal'
|
||||||
|
itemDetailsTemplate = require 'templates/play/modal/item-details-view'
|
||||||
|
|
||||||
CocoCollection = require 'collections/CocoCollection'
|
CocoCollection = require 'collections/CocoCollection'
|
||||||
ThangType = require 'models/ThangType'
|
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
|
module.exports = class PlayItemsModal extends ModalView
|
||||||
className: 'modal fade play-modal'
|
className: 'modal fade play-modal'
|
||||||
template: template
|
template: template
|
||||||
modalWidthPercent: 90
|
|
||||||
id: 'play-items-modal'
|
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:
|
events:
|
||||||
# 'change input.select': 'onSelectionChanged'
|
'click .item': 'onItemClicked'
|
||||||
|
'shown.bs.tab': 'onTabClicked'
|
||||||
|
'click .unlock-button': 'onUnlockButtonClicked'
|
||||||
|
'click #close-modal': 'hide'
|
||||||
|
|
||||||
constructor: (options) ->
|
constructor: (options) ->
|
||||||
super options
|
super options
|
||||||
@items = new CocoCollection([], {model: ThangType})
|
me.set('spent', 0)
|
||||||
@items.url = '/db/thang.type?view=items&project=name,description,components,original,rasterIcon'
|
@items = new Backbone.Collection()
|
||||||
@supermodel.loadCollection(@items, 'items')
|
@itemCategoryCollections = {}
|
||||||
|
|
||||||
groupItems: ->
|
project = [
|
||||||
groups = {}
|
'name'
|
||||||
for item in @items.models
|
'components.config'
|
||||||
itemSlots = item.getAllowedSlots()
|
'components.original'
|
||||||
for group, groupSlots of @slotGroups
|
'slug'
|
||||||
if _.find itemSlots, ((slot) -> slot in groupSlots)
|
'original'
|
||||||
groups[group] ?= []
|
'rasterIcon'
|
||||||
groups[group].push item
|
'gems'
|
||||||
groups
|
'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 = {}
|
||||||
|
|
||||||
|
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={}) ->
|
getRenderData: (context={}) ->
|
||||||
context = super(context)
|
context = super(context)
|
||||||
context.slotGroups = @groupItems()
|
context.itemCategoryCollections = @itemCategoryCollections
|
||||||
context.slotGroupsArray = _.keys context.slotGroups
|
context.itemCategories = _.keys @itemCategoryCollections
|
||||||
context.slotGroupsNames = ($.i18n.t "items.#{slotGroup}" for slotGroup in context.slotGroupsArray)
|
context.itemCategoryNames = ($.i18n.t "items.#{category}" for category in context.itemCategories)
|
||||||
|
context.gems = me.gems()
|
||||||
context
|
context
|
||||||
|
|
||||||
afterRender: ->
|
afterRender: ->
|
||||||
super()
|
super()
|
||||||
return unless @supermodel.finished()
|
return unless @supermodel.finished()
|
||||||
Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'game-menu-open', volume: 1
|
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: ->
|
onHidden: ->
|
||||||
super()
|
super()
|
||||||
Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'game-menu-close', volume: 1
|
Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'game-menu-close', volume: 1
|
||||||
|
|
||||||
addItemViews: ->
|
|
||||||
keys = (item.id for item in @items.models)
|
#- Click events
|
||||||
itemMap = _.zipObject keys, @items.models
|
|
||||||
for itemStub in @$el.find('.replace-me')
|
onItemClicked: (e) ->
|
||||||
itemID = $(itemStub).data('item-id')
|
return if $(e.target).closest('.unlock-button').length
|
||||||
item = itemMap[itemID]
|
itemEl = $(e.target).closest('.item')
|
||||||
itemView = new ItemView({item: item, includes: {name: true, stats: true, props: true}})
|
wasSelected = itemEl.hasClass('selected')
|
||||||
itemView.render()
|
@$el.find('.item.selected').removeClass('selected')
|
||||||
$(itemStub).replaceWith(itemView.$el)
|
if wasSelected
|
||||||
@registerSubView(itemView)
|
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_session': 'levels/sessions/level_session_handler'
|
||||||
'level_system': 'levels/systems/level_system_handler'
|
'level_system': 'levels/systems/level_system_handler'
|
||||||
'patch': 'patches/patch_handler'
|
'patch': 'patches/patch_handler'
|
||||||
|
'purchase': 'purchases/purchase_handler'
|
||||||
'thang_type': 'levels/thangs/thang_type_handler'
|
'thang_type': 'levels/thangs/thang_type_handler'
|
||||||
'user': 'users/user_handler'
|
'user': 'users/user_handler'
|
||||||
'user_code_problem': 'user_code_problems/user_code_problem_handler'
|
'user_code_problem': 'user_code_problems/user_code_problem_handler'
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
LevelComponent = require './LevelComponent'
|
LevelComponent = require './LevelComponent'
|
||||||
Handler = require '../../commons/Handler'
|
Handler = require '../../commons/Handler'
|
||||||
|
mongoose = require 'mongoose'
|
||||||
|
|
||||||
LevelComponentHandler = class LevelComponentHandler extends Handler
|
LevelComponentHandler = class LevelComponentHandler extends Handler
|
||||||
modelClass: LevelComponent
|
modelClass: LevelComponent
|
||||||
|
@ -22,4 +23,30 @@ LevelComponentHandler = class LevelComponentHandler extends Handler
|
||||||
props.push('official') if req.user?.isAdmin()
|
props.push('official') if req.user?.isAdmin()
|
||||||
props
|
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()
|
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()
|
|
@ -60,6 +60,12 @@ UserSchema.methods.setEmailSubscription = (newName, enabled) ->
|
||||||
@set('emails', newSubs)
|
@set('emails', newSubs)
|
||||||
@newsSubsChanged = true if newName in mail.NEWS_GROUPS
|
@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) ->
|
UserSchema.methods.isEmailSubscriptionEnabled = (newName) ->
|
||||||
emails = @get 'emails'
|
emails = @get 'emails'
|
||||||
if not emails
|
if not emails
|
||||||
|
|