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.

This commit is contained in:
Scott Erickson 2014-11-01 14:15:57 -07:00
parent 54a9497a4f
commit 33d14918b0
41 changed files with 856 additions and 88 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -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'

View file

@ -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'

View file

@ -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:

View 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
}
})

View file

@ -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
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?

View file

@ -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

View 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

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -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

View file

@ -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

View file

@ -21,7 +21,7 @@
.overlay-text
color: $yellow
font-family: Bangers
font-family: Open Sans Condensed
@include transition(color .10s linear)
.level-difficulty

View file

@ -55,4 +55,4 @@ body.ipad #level-victory-modal
.modal-body
font-size: 30px
font-family: Bangers
font-family: Open Sans Condensed

View file

@ -1,8 +1,347 @@
#play-items-modal
.item-view
width: 420px
float: left
background-color: white
border-radius: 6px
margin: 5px
@import "app/styles/mixins"
#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)

View 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

View file

@ -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
h3(data-i18n="play.items") Items
h1.big-font(data-i18n="play.items")
block modal-body-content
ul.nav.nav-tabs
for slotGroup, index in slotGroupsArray
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(href="#slot-group-" + slotGroup, data-toggle="tab")
span= slotGroupsNames[index]
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 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)
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

View file

@ -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()

View file

@ -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

View file

@ -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

View file

@ -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)'

View file

@ -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 = {}
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
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 = {}
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

View file

@ -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'

View file

@ -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()

View 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)

View 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()

View file

@ -60,6 +60,12 @@ UserSchema.methods.setEmailSubscription = (newName, 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'
if not emails