Merge branch 'master' into production

This commit is contained in:
Nick Winter 2014-11-12 18:44:53 -08:00
commit 6b92df2e41
33 changed files with 378 additions and 111 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View file

@ -40,12 +40,17 @@ module.exports = class SpriteParser
if movieClips.length if movieClips.length
# First movie clip is root, so do it last # First movie clip is root, so do it last
movieClips = movieClips[1 ... movieClips.length].concat([movieClips[0]]) movieClips = movieClips[1 ... movieClips.length].concat([movieClips[0]])
else if containers.length
# First container is root, so do it last # first container isn't necessarily root... actually the last one is root in blue-cart
containers = containers[1 ... containers.length].concat([containers[0]]) # else if containers.length
# # First container is root, so do it last
# containers = containers[1 ... containers.length].concat([containers[0]])
mainClip = _.last(movieClips) ? _.last(containers) mainClip = _.last(movieClips) ? _.last(containers)
@animationName = mainClip.name @animationName = mainClip.name
for container in containers for container, index in containers
if index is containers.length - 1 and not movieClips.length and container.bounds?.length
container.bounds[0] -= @width / 2
container.bounds[1] -= @height / 2
[shapeKeys, localShapes] = @getShapesFromBlock container, source [shapeKeys, localShapes] = @getShapesFromBlock container, source
localContainers = @getContainersFromMovieClip container, source localContainers = @getContainersFromMovieClip container, source
addChildArgs = @getAddChildCallArguments container, source addChildArgs = @getAddChildCallArguments container, source
@ -62,6 +67,7 @@ module.exports = class SpriteParser
if c.bn is bn if c.bn is bn
instructions.push {t: c.t, gn: c.gn} instructions.push {t: c.t, gn: c.gn}
break break
continue unless container.bounds and instructions.length
@addContainer {c: instructions, b: container.bounds}, container.name @addContainer {c: instructions, b: container.bounds}, container.name
for movieClip, index in movieClips for movieClip, index in movieClips
if index is 0 if index is 0

View file

@ -311,6 +311,11 @@
equip: "Equip" equip: "Equip"
unequip: "Unequip" unequip: "Unequip"
buy_gems:
few_gems: 'A few gems'
pile_gems: 'Pile of gems'
chest_gems: 'Chest of gems'
choose_hero: choose_hero:
choose_hero: "Choose Your Hero" choose_hero: "Choose Your Hero"
programming_language: "Programming Language" programming_language: "Programming Language"

View file

@ -240,7 +240,6 @@ module.exports = class ThangType extends CocoModel
return if _.isString spriteSheet return if _.isString spriteSheet
return unless spriteSheet return unless spriteSheet
canvas = $("<canvas width='#{size}' height='#{size}'></canvas>") canvas = $("<canvas width='#{size}' height='#{size}'></canvas>")
console.log 'made canvas', canvas, 'with size', size unless canvas[0]
stage = new createjs.Stage(canvas[0]) stage = new createjs.Stage(canvas[0])
sprite = new createjs.Sprite(spriteSheet) sprite = new createjs.Sprite(spriteSheet)
pt = @actions.portrait?.positions?.registration pt = @actions.portrait?.positions?.registration
@ -262,6 +261,29 @@ module.exports = class ThangType extends CocoModel
@tick = null @tick = null
stage stage
getVectorPortraitStage: (size=100) ->
return unless @actions
canvas = $("<canvas width='#{size}' height='#{size}'></canvas>")
stage = new createjs.Stage(canvas[0])
portrait = @actions.portrait
return unless portrait and (portrait.animation or portrait.container)
scale = portrait.scale or 1
vectorParser = new SpriteBuilder(@, {})
if portrait.animation
sprite = vectorParser.buildMovieClip portrait.animation
sprite.gotoAndStop(0)
else if portrait.container
sprite = vectorParser.buildContainerFromStore(portrait.container)
pt = portrait.positions?.registration
sprite.regX = pt?.x or 0
sprite.regY = pt?.y or 0
sprite.scaleX = sprite.scaleY = scale * size / 100
stage.addChild(sprite)
stage.update()
stage
uploadGenericPortrait: (callback, src) -> uploadGenericPortrait: (callback, src) ->
src ?= @getPortraitSource() src ?= @getPortraitSource()
return callback?() unless src and src.startsWith 'data:' return callback?() unless src and src.startsWith 'data:'

View file

@ -224,6 +224,7 @@ LevelSchema = c.object {
c.extendNamedProperties LevelSchema # let's have the name be the first property c.extendNamedProperties LevelSchema # let's have the name be the first property
_.extend LevelSchema.properties, _.extend LevelSchema.properties,
description: {title: 'Description', description: 'A short explanation of what this level is about.', type: 'string', maxLength: 65536, format: 'markdown'} description: {title: 'Description', description: 'A short explanation of what this level is about.', type: 'string', maxLength: 65536, format: 'markdown'}
loadingTip: { type: 'string', title: 'Loading Tip', description: 'What to show for this level while it\'s loading.' }
documentation: c.object {title: 'Documentation', description: 'Documentation articles relating to this level.', required: ['specificArticles', 'generalArticles'], 'default': {specificArticles: [], generalArticles: []}}, documentation: c.object {title: 'Documentation', description: 'Documentation articles relating to this level.', required: ['specificArticles', 'generalArticles'], 'default': {specificArticles: [], generalArticles: []}},
specificArticles: c.array {title: 'Specific Articles', description: 'Specific documentation articles that live only in this level.', uniqueItems: true }, SpecificArticleSchema specificArticles: c.array {title: 'Specific Articles', description: 'Specific documentation articles that live only in this level.', uniqueItems: true }, SpecificArticleSchema
generalArticles: c.array {title: 'General Articles', description: 'General documentation articles that can be linked from multiple levels.', uniqueItems: true}, GeneralArticleSchema generalArticles: c.array {title: 'General Articles', description: 'General documentation articles that can be linked from multiple levels.', uniqueItems: true}, GeneralArticleSchema
@ -243,7 +244,7 @@ _.extend LevelSchema.properties,
body: {type: 'string', format: 'markdown', title: 'Body Text', description: 'Inserted into the Victory Modal once this level is complete. Tell the player they did a good job and what they accomplished!'}, body: {type: 'string', format: 'markdown', title: 'Body Text', description: 'Inserted into the Victory Modal once this level is complete. Tell the player they did a good job and what they accomplished!'},
i18n: {type: 'object', format: 'i18n', props: ['body'], description: 'Help translate this victory message'} i18n: {type: 'object', format: 'i18n', props: ['body'], description: 'Help translate this victory message'}
} }
i18n: {type: 'object', format: 'i18n', props: ['name', 'description'], description: 'Help translate this level'} i18n: {type: 'object', format: 'i18n', props: ['name', 'description', 'loadingTip'], description: 'Help translate this level'}
icon: {type: 'string', format: 'image-file', title: 'Icon'} icon: {type: 'string', format: 'image-file', title: 'Icon'}
banner: {type: 'string', format: 'image-file', title: 'Banner'} banner: {type: 'string', format: 'image-file', title: 'Banner'}
goals: c.array {title: 'Goals', description: 'An array of goals which are visible to the player and can trigger scripts.'}, GoalSchema goals: c.array {title: 'Goals', description: 'An array of goals which are visible to the player and can trigger scripts.'}, GoalSchema

View file

@ -130,6 +130,7 @@ _.extend ThangTypeSchema.properties,
positions: PositionsSchema positions: PositionsSchema
raster: {type: 'string', format: 'image-file', title: 'Raster Image'} raster: {type: 'string', format: 'image-file', title: 'Raster Image'}
rasterIcon: { type: 'string', format: 'image-file', title: 'Raster Image Icon' } rasterIcon: { type: 'string', format: 'image-file', title: 'Raster Image Icon' }
containerIcon: { type: 'string' }
featureImage: { type: 'string', format: 'image-file', title: 'Feature Image' } featureImage: { type: 'string', format: 'image-file', title: 'Feature Image' }
colorGroups: c.object colorGroups: c.object
title: 'Color Groups' title: 'Color Groups'

View file

@ -4,9 +4,8 @@ module.exports =
'ipad:products': c.object {required: ['products']}, 'ipad:products': c.object {required: ['products']},
products: c.array {}, products: c.array {},
c.object {}, c.object {},
gems: { type: 'integer' }
price: { type: 'string' } price: { type: 'string' }
id: { type: 'string' } id: { type: 'string' }
'ipad:iap-complete': c.object {}, 'ipad:iap-complete': c.object {},
gems: { type: 'integer' } productID: { type: 'string' }

View file

@ -0,0 +1,9 @@
#vector-icon-setup-modal
select
margin-bottom: 20px
canvas
background: lightblue
border: 3px solid black
margin: 20px auto
display: block

View file

@ -40,7 +40,7 @@ $itemSlotGridHeight: 70px
#gems-count-container #gems-count-container
position: absolute position: absolute
left: 213px left: 374px
top: 10px top: 10px
width: 160px width: 160px
height: 66px height: 66px
@ -49,7 +49,7 @@ $itemSlotGridHeight: 70px
#gems-count #gems-count
position: absolute position: absolute
left: 75px left: 75px
top: 17px top: 14px
font-size: 25px font-size: 25px
color: rgb(1,64,91) color: rgb(1,64,91)
@ -58,8 +58,8 @@ $itemSlotGridHeight: 70px
#close-modal #close-modal
position: absolute position: absolute
left: 390px left: 551px
top: 23px top: 21px
width: 60px width: 60px
height: 60px height: 60px
color: white color: white
@ -77,11 +77,9 @@ $itemSlotGridHeight: 70px
#equipped #equipped
width: 330px width: 330px
background: white
border: 3px solid black
position: absolute position: absolute
left: 20px left: 20px
top: 112px top: 122px
height: 450px height: 450px
overflow: hidden overflow: hidden
@ -307,14 +305,12 @@ $itemSlotGridHeight: 70px
position: absolute position: absolute
#unequipped #unequipped
width: 222px width: 245px
position: absolute position: absolute
left: 370px left: 370px
top: 112px top: 135px
height: 450px height: 435px
border: 3px solid black
padding: 9px 0 9px 9px padding: 9px 0 9px 9px
background-color: white
#double-click-hint #double-click-hint
margin: 20px 0 70px margin: 20px 0 70px
@ -387,12 +383,12 @@ $itemSlotGridHeight: 70px
//- Hero/Play buttons //- Hero/Play buttons
#choose-hero-button, #play-level-button #choose-hero-button, #play-level-button
top: 572px top: 582px
position: absolute position: absolute
background: url(/images/pages/play/modal/confirm-button.png) background: url(/images/pages/play/modal/confirm-button.png)
width: 209px width: 209px
height: 65px height: 55px
background-size: 209px 65px background-size: 209px 55px
border: 0 border: 0
&:disabled &:disabled

View file

@ -1,8 +1,44 @@
#buy-gems-modal #buy-gems-modal
button
width: 100%
margin-bottom: 10px
.gem //- Clear modal defaults
width: 20px .modal-dialog
height: 20px margin: 100px auto 0 auto
padding: 0
width: 820px
height: 460px
background: none
//- Background
#buy-gems-background
position: absolute
top: -157px
left: -2px
//- Products
#products
position: absolute
left: 55px
top: 242px
width: 720px
height: 140px
index: 1
.product
width: 228px
overflow: none
float: left
text-align: center
margin-right: 12px
h4
font-size: 20px
color: rgb(22,16,5)
h3
margin-top: 10px
text-transform: uppercase
color: rgb(22,16,5)
button
width: 80%

View file

@ -195,6 +195,26 @@ $gameControlMargin: 30px
margin: 10px auto 0 auto margin: 10px auto 0 auto
width: 200px width: 200px
.campaign-switch
color: purple
position: absolute
z-index: 1
font-size: 2vw
text-shadow: 0 0 0.3vw white, 0 0 0.3vw white
&:hover
text-decoration: none
&#forest-link
left: 94.5%
top: 7%
transform: rotate(-35deg)
&#dungeon-link
left: 26.6%
top: 43%
transform: rotate(180deg)
.game-controls .game-controls
position: absolute position: absolute
right: 1% right: 1%
@ -239,6 +259,8 @@ $gameControlMargin: 30px
background-position: (-3 * $gameControlSize) 0px background-position: (-3 * $gameControlSize) 0px
&.settings &.settings
background-position: (-4 * $gameControlSize) 0px background-position: (-4 * $gameControlSize) 0px
&.gems
background-position: (-5 * $gameControlSize) 0px
.tooltip .tooltip
font-size: 24px font-size: 24px

View file

@ -81,12 +81,15 @@ block outer_content
div#settings-col.well div#settings-col.well
img#portrait.img-thumbnail img#portrait.img-thumbnail
div.file-controls div.file-controls
button(disabled=authorized === true ? undefined : "true").btn.btn-small.btn-info#upload-button button(disabled=authorized === true ? undefined : "true").btn.btn-sm.btn-info#upload-button
span.glyphicon.glyphicon-upload span.glyphicon.glyphicon-upload
span.spl Upload Animation span.spl Upload Animation
button(disabled=authorized === true ? undefined : "true").btn.btn-small.btn-danger#clear-button button(disabled=authorized === true ? undefined : "true").btn.btn-sm.btn-danger#clear-button
span.glyphicon.glyphicon-remove span.glyphicon.glyphicon-remove
span.spl Clear Data span.spl Clear Data
button#set-vector-icon(disabled=authorized === true ? undefined : "true").btn.btn-sm
span.glyphicon.glyphicon-gbp
span.spl Vector Icon Setup
input#real-upload-button(type="file") input#real-upload-button(type="file")
#thang-type-file-size= fileSizeString #thang-type-file-size= fileSizeString
div#thang-type-treema div#thang-type-treema

View file

@ -0,0 +1,24 @@
extends /templates/modal/modal_base
block modal-header-content
h3 Choose Container for Vector Icon
block modal-body-content
if chosenContainer
form.form
.form-group
select#container-select.form-control
for container in containers
option(value=container, selected=container === chosenContainer)= container
canvas(width=demoSize height=demoSize)#resulting-icon
.alert.alert-info Arrow keys to move, Shift-Plus/Minus to scale.
else
div forgetting something?
block modal-footer-content
button.btn.pull-left.btn-info#center
span.glyphicon.glyphicon-cutlery
span.spl Center
button.btn.btn-primary#done-button Done

View file

@ -1,6 +1,6 @@
.modal-dialog .modal-dialog
.modal-content .modal-content
img(src="/images/pages/play/modal/items-background-narrow.png")#play-items-modal-narrow-bg img(src="/images/pages/play/modal/inventory-background.png")#play-items-modal-narrow-bg
div#gems-count-container div#gems-count-container
span#gems-count.big-font= gems span#gems-count.big-font= gems

View file

@ -1,14 +1,11 @@
extends /templates/modal/modal_base .modal-dialog
.modal-content
img(src="/images/pages/play/modal/buy-gems-background.png")#buy-gems-background
block modal-header-content #products
h3(data-i18n='play.buy_gems') for product in products
.product
block modal-body-content h4 x#{product.gems}
for product in products h3(data-i18n=product.i18n)
button.product.btn.btn-lg(value=product.id) button.btn.btn-illustrated.btn-lg(value=product.id)
img.gem(src="/images/common/gem.png") span= product.price
span x#{product.gems): #{product.price}
block modal-footer
.modal-footer
button(data-dismiss="modal", data-i18n="common.cancel").btn Cancel

View file

@ -20,7 +20,7 @@
if props.length if props.length
#skills #skills
h3.big-font(data-i18n="play.skills-granted") h3.big-font(data-i18n="play.skills_granted")
for prop in props for prop in props
p p
strong.big-font= prop.name strong.big-font= prop.name

View file

@ -35,12 +35,17 @@
.campaign-label(style="color: #{campaign.color}")= campaign.name .campaign-label(style="color: #{campaign.color}")= campaign.name
if isIPadApp && !level.disabled && !level.locked if isIPadApp && !level.disabled && !level.locked
button.btn.btn-success.btn-lg.start-level(data-i18n="common.play") Play button.btn.btn-success.btn-lg.start-level(data-i18n="common.play") Play
if mapType === 'dungeon' && forestIsAvailable
a#forest-link.glyphicon.glyphicon-share-alt.campaign-switch(href="/play/forest", data-i18n="[title]play.campaign_forest")
if mapType === 'forest'
a#dungeon-link.glyphicon.glyphicon-share-alt.campaign-switch(href="/play/dungeon", data-i18n="[title]play.campaign_dungeon")
.game-controls.header-font .game-controls.header-font
button.btn.items(data-toggle='coco-modal', data-target='play/modal/PlayItemsModal', data-i18n="[title]play.items") button.btn.items(data-toggle='coco-modal', data-target='play/modal/PlayItemsModal', data-i18n="[title]play.items")
button.btn.heroes(data-toggle='coco-modal', data-target='play/modal/PlayHeroesModal', data-i18n="[title]play.heroes") button.btn.heroes(data-toggle='coco-modal', data-target='play/modal/PlayHeroesModal', data-i18n="[title]play.heroes")
if me.isAdmin() if me.isAdmin() || isIPadApp
button.btn.gems(data-toggle='coco-modal', data-target='play/modal/BuyGemsModal', data-i18n="[title]play.buy_gems") button.btn.gems(data-toggle='coco-modal', data-target='play/modal/BuyGemsModal', data-i18n="[title]play.buy_gems")
if me.isAdmin()
button.btn.achievements(data-toggle='coco-modal', data-target='play/modal/PlayAchievementsModal', data-i18n="[title]play.achievements") button.btn.achievements(data-toggle='coco-modal', data-target='play/modal/PlayAchievementsModal', data-i18n="[title]play.achievements")
button.btn.account(data-toggle='coco-modal', data-target='play/modal/PlayAccountModal', data-i18n="[title]play.account") button.btn.account(data-toggle='coco-modal', data-target='play/modal/PlayAccountModal', data-i18n="[title]play.account")
button.btn.settings(data-toggle='coco-modal', data-target='play/modal/PlaySettingsModal', data-i18n="[title]play.settings") button.btn.settings(data-toggle='coco-modal', data-target='play/modal/PlaySettingsModal', data-i18n="[title]play.settings")
@ -52,13 +57,6 @@
// a.btn.account(href="/user/#{me.getSlugOrID()}", data-i18n="[title]play.account") // a.btn.account(href="/user/#{me.getSlugOrID()}", data-i18n="[title]play.account")
// a.btn.settings(href='/account', data-i18n="[title]play.settings") // a.btn.settings(href='/account', data-i18n="[title]play.settings")
if mapType === 'forest'
a.btn.campaign-switch(href="/play/dungeon", data-i18n="[title]play.campaign_dungeon")
img(src="/images/pages/play/map_dungeon_icon.jpg").img-thumbnail
if mapType === 'dungeon'
a.btn.campaign-switch(href="/play/forest", data-i18n="[title]play.campaign_forest")
img(src="/images/pages/play/map_forest_icon.jpg").img-thumbnail
.old-levels .old-levels
a(href="/play-old", data-i18n="play.older_campaigns").header-font Older Campaigns a(href="/play-old", data-i18n="play.older_campaigns").header-font Older Campaigns

View file

@ -13,7 +13,7 @@ module.exports = class SettingsTabView extends CocoView
# not thangs or scripts or the backend stuff # not thangs or scripts or the backend stuff
editableSettings: [ editableSettings: [
'name', 'description', 'documentation', 'nextLevel', 'background', 'victory', 'i18n', 'icon', 'goals', 'name', 'description', 'documentation', 'nextLevel', 'background', 'victory', 'i18n', 'icon', 'goals',
'type', 'terrain', 'showsGuide', 'banner', 'employerDescription' 'type', 'terrain', 'showsGuide', 'banner', 'employerDescription', 'loadingTip'
] ]
subscriptions: subscriptions:

View file

@ -12,6 +12,7 @@ ThangTypeVersionsModal = require './ThangTypeVersionsModal'
ThangTypeColorsTabView = require './ThangTypeColorsTabView' ThangTypeColorsTabView = require './ThangTypeColorsTabView'
PatchesView = require 'views/editor/PatchesView' PatchesView = require 'views/editor/PatchesView'
ForkModal = require 'views/editor/ForkModal' ForkModal = require 'views/editor/ForkModal'
VectorIconSetupModal = require 'views/editor/thang/VectorIconSetupModal'
SaveVersionModal = require 'views/modal/SaveVersionModal' SaveVersionModal = require 'views/modal/SaveVersionModal'
template = require 'templates/editor/thang/thang-type-edit-view' template = require 'templates/editor/thang/thang-type-edit-view'
storage = require 'lib/storage' storage = require 'lib/storage'
@ -33,6 +34,7 @@ module.exports = class ThangTypeEditView extends RootView
events: events:
'click #clear-button': 'clearRawData' 'click #clear-button': 'clearRawData'
'click #upload-button': -> @$el.find('input#real-upload-button').click() 'click #upload-button': -> @$el.find('input#real-upload-button').click()
'click #set-vector-icon': 'onClickSetVectorIcon'
'change #real-upload-button': 'animationFileChosen' 'change #real-upload-button': 'animationFileChosen'
'change #animations-select': 'showAnimation' 'change #animations-select': 'showAnimation'
'click #marker-button': 'toggleDots' 'click #marker-button': 'toggleDots'
@ -47,6 +49,12 @@ module.exports = class ThangTypeEditView extends RootView
'keyup .play-with-level-input': 'onPlayLevelKeyUp' 'keyup .play-with-level-input': 'onPlayLevelKeyUp'
'click #pop-level-i18n-button': 'onPopulateLevelI18N' 'click #pop-level-i18n-button': 'onPopulateLevelI18N'
onClickSetVectorIcon: ->
modal = new VectorIconSetupModal({}, @thangType)
@openModalView modal
modal.once 'done', => @treema.set('/', @getThangData())
subscriptions: subscriptions:
'editor:thang-type-color-groups-changed': 'onColorGroupsChanged' 'editor:thang-type-color-groups-changed': 'onColorGroupsChanged'
'editor:save-new-version': 'saveNewThangType' 'editor:save-new-version': 'saveNewThangType'
@ -234,7 +242,7 @@ module.exports = class ThangTypeEditView extends RootView
# animation select # animation select
refreshAnimation: => refreshAnimation: =>
@thangType.buildActions() @thangType.resetSpriteSheetCache()
return @showRasterImage() if @thangType.get('raster') return @showRasterImage() if @thangType.get('raster')
options = @getLankOptions() options = @getLankOptions()
console.log 'refresh animation....' console.log 'refresh animation....'

View file

@ -0,0 +1,115 @@
ModalView = require 'views/kinds/ModalView'
template = require 'templates/editor/thang/vector-icon-setup-modal'
module.exports = class VectorIconSetupModal extends ModalView
id: "vector-icon-setup-modal"
template: template
demoSize: 400
plain: true
events:
'change #container-select': 'onChangeContainer'
'click #center': 'onClickCenter'
'click #zero-bounds': 'onClickZeroBounds'
'click #done-button': 'onClickDone'
shortcuts:
'shift+-': -> @incrScale(-0.02)
'shift+=': -> @incrScale(0.02)
'up': -> @incrRegY(1)
'down': -> @incrRegY(-1)
'left': -> @incrRegX(1)
'right': -> @incrRegX(-1)
constructor: (options, @thangType) ->
portrait = @thangType.get('actions')?.portrait
@containers = _.keys(@thangType.get('raw')?.containers or {})
@container = portrait?.container or _.last @containers
@scale = portrait?.scale or 1
@regX = portrait?.positions?.registration?.x or 0
@regY = portrait?.positions?.registration?.y or 0
@saveChanges()
super(options)
saveChanges: ->
actions = _.cloneDeep (@thangType.get('actions') ? {})
actions.portrait ?= {}
actions.portrait.scale = @scale
actions.portrait.positions ?= {}
actions.portrait.positions.registration = { x: @regX, y: @regY }
actions.portrait.container = @container
@thangType.set('actions', actions)
@thangType.buildActions()
getRenderData: ->
c = super()
c.containers = @containers
c.chosenContainer = @container
c.demoSize = @demoSize
c
afterRender: ->
@initStage()
super()
initStage: ->
return unless @containers and @container
@stage = @thangType.getVectorPortraitStage(@demoSize)
@sprite = @stage.children[0]
canvas = $(@stage.canvas)
canvas.attr('id', 'resulting-icon')
@$el.find('#resulting-icon').replaceWith(canvas)
@updateSpriteProperties()
onChangeContainer: (e) ->
@container = $(e.target).val()
@saveChanges()
@initStage()
refreshSprite: ->
return unless @stage
stage = @thangType.getVectorPortraitStage(@demoSize)
@stage.removeAllChildren()
@stage.addChild(@sprite = stage.children[0])
@updateSpriteProperties()
@stage.update()
updateSpriteProperties: ->
@sprite.scaleX = @sprite.scaleY = @scale * @demoSize / 100
@sprite.regX = @regX
@sprite.regY = @regY
console.log 'set to', @scale, @regX, @regY
onClickCenter: ->
containerInfo = @thangType.get('raw').containers[@container]
b = containerInfo.b
@regX = b[0]
@regY = b[1]
maxDimension = Math.max(b[2], b[3])
@scale = 100 / maxDimension
if b[2] > b[3]
@regY += (b[3] - b[2]) / 2
else
@regX += (b[2] - b[3]) / 2
@updateSpriteProperties()
@stage.update()
incrScale: (amount) ->
@scale += amount
@updateSpriteProperties()
@stage.update()
incrRegX: (amount) ->
@regX += amount
@updateSpriteProperties()
@stage.update()
incrRegY: (amount) ->
@regY += amount
@updateSpriteProperties()
@stage.update()
onClickDone: ->
@saveChanges()
@trigger 'done'
@hide()

View file

@ -124,7 +124,7 @@ module.exports = class InventoryModal extends ModalView
@itemDetailsView = new ItemDetailsView() @itemDetailsView = new ItemDetailsView()
@insertSubView(@itemDetailsView) @insertSubView(@itemDetailsView)
@requireLevelEquipment() @requireLevelEquipment()
@$el.find('.nano').nanoScroller() @$el.find('.nano').nanoScroller({alwaysVisible: true})
afterInsert: -> afterInsert: ->
super() super()

View file

@ -15,6 +15,8 @@ module.exports = class I18NEditLevelView extends I18NEditModelView
@wrapRow "Level name", ['name'], name, i18n[lang]?.name, [] @wrapRow "Level name", ['name'], name, i18n[lang]?.name, []
if description = @model.get('description') if description = @model.get('description')
@wrapRow "Level description", ['description'], description, i18n[lang]?.description, [] @wrapRow "Level description", ['description'], description, i18n[lang]?.description, []
if loadingTip = @model.get('loadingTip')
@wrapRow "Loading tip", ['loadingTip'], loadingTip, i18n[lang]?.loadingTip, []
# goals # goals
for goal, index in @model.get('goals') ? [] for goal, index in @model.get('goals') ? []

View file

@ -34,6 +34,7 @@ module.exports = class RootView extends CocoView
'achievements:new': 'handleNewAchievements' 'achievements:new': 'handleNewAchievements'
showNewAchievement: (achievement, earnedAchievement) -> showNewAchievement: (achievement, earnedAchievement) ->
return if achievement.get('collection') is 'level.sessions'
popup = new AchievementPopup achievement: achievement, earnedAchievement: earnedAchievement popup = new AchievementPopup achievement: achievement, earnedAchievement: earnedAchievement
handleNewAchievements: (e) -> handleNewAchievements: (e) ->

View file

@ -100,6 +100,7 @@ module.exports = class WorldMapView extends RootView
context.isIPadApp = application.isIPadApp context.isIPadApp = application.isIPadApp
context.mapType = _.string.slugify @terrain context.mapType = _.string.slugify @terrain
context.nextLevel = @nextLevel context.nextLevel = @nextLevel
context.forestIsAvailable = '541b67f71ccc8eaae19f3c62' in (me.get('earned')?.levels or [])
context context
afterRender: -> afterRender: ->
@ -596,7 +597,7 @@ forest = [
description: 'Protect the peasants from the pursuing ogres.' description: 'Protect the peasants from the pursuing ogres.'
nextLevels: nextLevels:
continue: 'winding-trail' continue: 'winding-trail'
x: 29.63 x: 32.63
y: 53.69 y: 53.69
} }
{ {

View file

@ -18,7 +18,7 @@ module.exports = class LevelLoadingView extends CocoView
@$el.find('.tip.rare').remove() if _.random(1, 10) < 9 @$el.find('.tip.rare').remove() if _.random(1, 10) < 9
tips = @$el.find('.tip').addClass('to-remove') tips = @$el.find('.tip').addClass('to-remove')
tip = _.sample(tips) tip = _.sample(tips)
$(tip).removeClass('to-remove') $(tip).removeClass('to-remove').addClass('secret')
@$el.find('.to-remove').remove() @$el.find('.to-remove').remove()
@onLevelLoaded level: @options.level if @options.level?.get('goals') # If Level was already loaded. @onLevelLoaded level: @options.level if @options.level?.get('goals') # If Level was already loaded.
@ -47,6 +47,11 @@ module.exports = class LevelLoadingView extends CocoView
goalContainer.removeClass('secret') goalContainer.removeClass('secret')
if goalCount is 1 if goalCount is 1
goalContainer.find('.panel-heading').text $.i18n.t 'play_level.goal' # Not plural goalContainer.find('.panel-heading').text $.i18n.t 'play_level.goal' # Not plural
tip = @$el.find('.tip')
if @level.get('loadingTip')
loadingTip = utils.i18n @level.attributes, 'loadingTip'
tip.text(loadingTip)
tip.removeClass('secret')
showReady: -> showReady: ->
return if @shownReady return if @shownReady
@ -59,8 +64,6 @@ module.exports = class LevelLoadingView extends CocoView
@startUnveiling() @startUnveiling()
@unveil() @unveil()
else else
ready = $.i18n.t('play_level.loading_ready', defaultValue: 'Ready!')
@$el.find('#tip-wrapper .tip').addClass('ready').text ready
Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'level_loaded', volume: 0.75 # old: loading_ready Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'level_loaded', volume: 0.75 # old: loading_ready
@$el.find('.progress').addClass 'active progress-striped' @$el.find('.progress').addClass 'active progress-striped'
@$el.find('.start-level-button').removeClass 'secret' @$el.find('.start-level-button').removeClass 'secret'

View file

@ -427,6 +427,7 @@ module.exports = class PlayLevelView extends RootView
application.tracker?.trackTiming victoryTime, 'Level Victory Time', @levelID, @levelID, 100 application.tracker?.trackTiming victoryTime, 'Level Victory Time', @levelID, @levelID, 100
showVictory: -> showVictory: ->
@endHighlight()
options = {level: @level, supermodel: @supermodel, session: @session} options = {level: @level, supermodel: @supermodel, session: @session}
ModalClass = if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'] then HeroVictoryModal else VictoryModal ModalClass = if @level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'] then HeroVictoryModal else VictoryModal
victoryModal = new ModalClass(options) victoryModal = new ModalClass(options)

View file

@ -129,6 +129,7 @@ module.exports = class SpellPaletteView extends CocoView
storages = [storages] if _.isString storages storages = [storages] if _.isString storages
for storage in storages for storage in storages
props = _.reject @thang[storage] ? [], (prop) -> prop[0] is '_' # no private properties props = _.reject @thang[storage] ? [], (prop) -> prop[0] is '_' # no private properties
props = _.uniq props
added = _.sortBy(props).slice() added = _.sortBy(props).slice()
propGroups[owner] = (propGroups[owner] ? []).concat added propGroups[owner] = (propGroups[owner] ? []).concat added
count += added.length count += added.length
@ -181,14 +182,18 @@ module.exports = class SpellPaletteView extends CocoView
propsByItem = {} propsByItem = {}
propCount = 0 propCount = 0
itemsByProp = {} itemsByProp = {}
for slot, thangTypeName of @thang.inventoryThangTypeNames ? {} # Make sure that we get the spellbook first, then the primary hand, then anything else.
slots = _.sortBy _.keys(@thang.inventoryThangTypeNames ? {}), (slot) ->
if slot is 'left-hand' then 0 else if slot is 'right-hand' then 1 else 2
for slot in slots
thangTypeName = @thang.inventoryThangTypeNames[slot]
if item = itemThangTypes[thangTypeName] if item = itemThangTypes[thangTypeName]
unless item.get('components') unless item.get('components')
console.error 'Item', item, 'did not have any components when we went to assemble docs.' console.error 'Item', item, 'did not have any components when we went to assemble docs.'
for component in item.get('components') ? [] when component.config for component in item.get('components') ? [] when component.config
for owner, storages of propStorage for owner, storages of propStorage
if props = component.config[storages] if props = component.config[storages]
for prop in _.sortBy(props) when prop[0] isnt '_' # no private properties for prop in _.sortBy(props) when prop[0] isnt '_' and not itemsByProp[prop] # no private properties
propsByItem[item.get('name')] ?= [] propsByItem[item.get('name')] ?= []
propsByItem[item.get('name')].push owner: owner, prop: prop, item: item propsByItem[item.get('name')].push owner: owner, prop: prop, item: item
itemsByProp[prop] = item itemsByProp[prop] = item

View file

@ -7,9 +7,9 @@ module.exports = class BuyGemsModal extends ModalView
plain: true plain: true
products: [ products: [
{ price: '$4.99', gems: 5000, id: 'gems_5' } { price: '$4.99', gems: 5000, id: 'gems_5', i18n: 'buy_gems.few_gems' }
{ price: '$9.99', gems: 11000, id: 'gems_10' } { price: '$9.99', gems: 11000, id: 'gems_10', i18n: 'buy_gems.pile_gems' }
{ price: '$19.99', gems: 25000, id: 'gems_20' } { price: '$19.99', gems: 25000, id: 'gems_20', i18n: 'buy_gems.chest_gems' }
] ]
subscriptions: subscriptions:
@ -17,7 +17,7 @@ module.exports = class BuyGemsModal extends ModalView
'ipad:iap-complete': 'onIAPComplete' 'ipad:iap-complete': 'onIAPComplete'
events: events:
'click button.product': 'onClickProductButton' 'click .product button': 'onClickProductButton'
constructor: (options) -> constructor: (options) ->
super(options) super(options)
@ -31,7 +31,13 @@ module.exports = class BuyGemsModal extends ModalView
return c return c
onIPadProducts: (e) -> onIPadProducts: (e) ->
@products = e.products newProducts = []
for iapProduct in e.products
localProduct = _.find @products, { id: iapProduct.id }
continue unless localProduct
localProduct.price = iapProduct.price
newProducts.push localProduct
@products = newProducts
@render() @render()
onClickProductButton: (e) -> onClickProductButton: (e) ->
@ -42,11 +48,14 @@ module.exports = class BuyGemsModal extends ModalView
Backbone.Mediator.publish 'buy-gems-modal:purchase-initiated', { productID: productID } Backbone.Mediator.publish 'buy-gems-modal:purchase-initiated', { productID: productID }
else else
@$el.find('.modal-body').append($('<div class="alert alert-danger">Not implemented</div>')) application.tracker?.trackEvent 'Started purchase', {productID:productID}, ['Google Analytics']
alert('Not yet implemented, but thanks for trying!')
onIAPComplete: (e) -> onIAPComplete: (e) ->
product = _.find @products, { id: e.productID }
purchased = me.get('purchased') ? {} purchased = me.get('purchased') ? {}
purchased = _.clone purchased
purchased.gems ?= 0 purchased.gems ?= 0
purchased.gems += e.gems purchased.gems += product.gems
me.set('purchased', purchased) me.set('purchased', purchased)
@hide() @hide()

View file

@ -29,6 +29,7 @@ LevelHandler = class LevelHandler extends Handler
'employerDescription' 'employerDescription'
'terrain' 'terrain'
'i18nCoverage' 'i18nCoverage'
'loadingTip'
] ]
postEditableProperties: ['name'] postEditableProperties: ['name']

View file

@ -1,41 +1,43 @@
InventoryView = require 'views/game-menu/InventoryView' # TODO: rework this spec for the InventoryModal
thangTypes = [ #InventoryView = require 'views/game-menu/InventoryView'
{"_id":"boots-id","name":"Boots","original":"boots","components":[{"original":"524b85837fc0f6d519000020","majorVersion":0},{"original":"524b7b857fc0f6d519000012","majorVersion":0},{"original":"524b4150ff92f1f4f8000024","majorVersion":0},{"original":"53e12043b82921000051cdf9","majorVersion":0,"config":{"slots":["feet"],"programmableProperties":["move","targetPos"],"moreProgrammableProperties":[],"extraHUDProperties":["maxSpeed"],"stats":{"maxSpeed":{"factor":1}}}},{"original":"524b7b8c7fc0f6d519000013","majorVersion":0,"config":{"locomotionType":"running","maxSpeed":5,"maxAcceleration":100}},{"original":"524b75ad7fc0f6d519000001","majorVersion":0,"config":{"pos":{"x":39.08,"y":20.72,"z":0.5},"width":1,"height":1,"depth":1,"shape":"ellipsoid"}},{"original":"524b7b7c7fc0f6d519000011","majorVersion":0}]}, #
{"_id":"boots-of-leaping-id","name":"Boots of Leaping","original":"boots-of-leaping","components":[{"original":"524b85837fc0f6d519000020","majorVersion":0},{"original":"524b7b857fc0f6d519000012","majorVersion":0},{"original":"524b4150ff92f1f4f8000024","majorVersion":0},{"original":"53e12043b82921000051cdf9","majorVersion":0,"config":{"ownerID":"Tharin","slots":["feet"],"programmableProperties":["move","targetPos","jumpTo"],"moreProgrammableProperties":["jump"],"extraHUDProperties":["maxSpeed"],"stats":{"maxSpeed":{"factor":1.2}}}},{"original":"524b7b8c7fc0f6d519000013","majorVersion":0,"config":{"locomotionType":"running","maxSpeed":6,"maxAcceleration":100}},{"original":"524b1f54d768d916b5000001","majorVersion":0,"config":{"jumpHeight":3}},{"original":"5275392d69abdcb12401441e","majorVersion":0,"config":{"jumpSpeedFactor":1.5}},{"original":"524b75ad7fc0f6d519000001","majorVersion":0,"config":{"pos":{"x":39.08,"y":20.72,"z":0.5},"width":1,"height":1,"depth":1,"shape":"ellipsoid"}},{"original":"524b7b7c7fc0f6d519000011","majorVersion":0}]}, #thangTypes = [
{"_id":"crossbow-id","name":"Crossbow","original":"crossbow","components":[{"original":"524b85837fc0f6d519000020","majorVersion":0},{"original":"524b517fff92f1f4f8000046","majorVersion":0},{"original":"524b7b747fc0f6d519000010","majorVersion":0,"config":{"team":"humans"}},{"original":"524b7bc67fc0f6d51900001a","majorVersion":0,"config":{"missileThangID":"Arrow"}},{"original":"524b7ba57fc0f6d519000016","majorVersion":0,"config":{"attackDamage":5,"attackRange":20,"cooldown":0.6,"chasesWhenAttackingOutOfRange":true}},{"original":"524b3e3fff92f1f4f800000d","majorVersion":0},{"original":"524cbdc03ea855e0ab0000bb","majorVersion":0},{"original":"524b4150ff92f1f4f8000024","majorVersion":0},{"original":"53e12043b82921000051cdf9","majorVersion":0,"config":{"slots":["right-hand"],"programmableProperties":["attack","target","attackRange"],"moreProgrammableProperties":["attackXY","targetPos"],"extraHUDProperties":["attackDamage","attackRange"]}},{"original":"524b75ad7fc0f6d519000001","majorVersion":0,"config":{"pos":{"x":41.105000000000004,"y":31.6,"z":0.125},"width":1.5,"height":0.75,"depth":0.25,"shape":"box"}},{"original":"524b7b7c7fc0f6d519000011","majorVersion":0},{"original":"524b457bff92f1f4f8000031","majorVersion":0}]}, # {"_id":"boots-id","name":"Boots","original":"boots","components":[{"original":"524b85837fc0f6d519000020","majorVersion":0},{"original":"524b7b857fc0f6d519000012","majorVersion":0},{"original":"524b4150ff92f1f4f8000024","majorVersion":0},{"original":"53e12043b82921000051cdf9","majorVersion":0,"config":{"slots":["feet"],"programmableProperties":["move","targetPos"],"moreProgrammableProperties":[],"extraHUDProperties":["maxSpeed"],"stats":{"maxSpeed":{"factor":1}}}},{"original":"524b7b8c7fc0f6d519000013","majorVersion":0,"config":{"locomotionType":"running","maxSpeed":5,"maxAcceleration":100}},{"original":"524b75ad7fc0f6d519000001","majorVersion":0,"config":{"pos":{"x":39.08,"y":20.72,"z":0.5},"width":1,"height":1,"depth":1,"shape":"ellipsoid"}},{"original":"524b7b7c7fc0f6d519000011","majorVersion":0}]},
{"_id":"crude-glasses-id","name":"Crude Glasses","original":"crude-glasses","components":[{"original":"524b7b747fc0f6d519000010","majorVersion":0,"config":{"team":"humans"}},{"original":"524b4150ff92f1f4f8000024","majorVersion":0},{"original":"53e12043b82921000051cdf9","majorVersion":0,"config":{"slots":["eyes"],"programmableProperties":["pos","getEnemies"],"moreProgrammableProperties":["getItems","getFriends"]}},{"original":"524b75ad7fc0f6d519000001","majorVersion":0,"config":{"pos":{"x":33.230000000000004,"y":20.75,"z":2},"width":1,"height":2,"depth":1,"shape":"ellipsoid"}},{"original":"524b457bff92f1f4f8000031","majorVersion":0,"config":{"visualRange":50}}]} # {"_id":"boots-of-leaping-id","name":"Boots of Leaping","original":"boots-of-leaping","components":[{"original":"524b85837fc0f6d519000020","majorVersion":0},{"original":"524b7b857fc0f6d519000012","majorVersion":0},{"original":"524b4150ff92f1f4f8000024","majorVersion":0},{"original":"53e12043b82921000051cdf9","majorVersion":0,"config":{"ownerID":"Tharin","slots":["feet"],"programmableProperties":["move","targetPos","jumpTo"],"moreProgrammableProperties":["jump"],"extraHUDProperties":["maxSpeed"],"stats":{"maxSpeed":{"factor":1.2}}}},{"original":"524b7b8c7fc0f6d519000013","majorVersion":0,"config":{"locomotionType":"running","maxSpeed":6,"maxAcceleration":100}},{"original":"524b1f54d768d916b5000001","majorVersion":0,"config":{"jumpHeight":3}},{"original":"5275392d69abdcb12401441e","majorVersion":0,"config":{"jumpSpeedFactor":1.5}},{"original":"524b75ad7fc0f6d519000001","majorVersion":0,"config":{"pos":{"x":39.08,"y":20.72,"z":0.5},"width":1,"height":1,"depth":1,"shape":"ellipsoid"}},{"original":"524b7b7c7fc0f6d519000011","majorVersion":0}]},
] # {"_id":"crossbow-id","name":"Crossbow","original":"crossbow","components":[{"original":"524b85837fc0f6d519000020","majorVersion":0},{"original":"524b517fff92f1f4f8000046","majorVersion":0},{"original":"524b7b747fc0f6d519000010","majorVersion":0,"config":{"team":"humans"}},{"original":"524b7bc67fc0f6d51900001a","majorVersion":0,"config":{"missileThangID":"Arrow"}},{"original":"524b7ba57fc0f6d519000016","majorVersion":0,"config":{"attackDamage":5,"attackRange":20,"cooldown":0.6,"chasesWhenAttackingOutOfRange":true}},{"original":"524b3e3fff92f1f4f800000d","majorVersion":0},{"original":"524cbdc03ea855e0ab0000bb","majorVersion":0},{"original":"524b4150ff92f1f4f8000024","majorVersion":0},{"original":"53e12043b82921000051cdf9","majorVersion":0,"config":{"slots":["right-hand"],"programmableProperties":["attack","target","attackRange"],"moreProgrammableProperties":["attackXY","targetPos"],"extraHUDProperties":["attackDamage","attackRange"]}},{"original":"524b75ad7fc0f6d519000001","majorVersion":0,"config":{"pos":{"x":41.105000000000004,"y":31.6,"z":0.125},"width":1.5,"height":0.75,"depth":0.25,"shape":"box"}},{"original":"524b7b7c7fc0f6d519000011","majorVersion":0},{"original":"524b457bff92f1f4f8000031","majorVersion":0}]},
# {"_id":"crude-glasses-id","name":"Crude Glasses","original":"crude-glasses","components":[{"original":"524b7b747fc0f6d519000010","majorVersion":0,"config":{"team":"humans"}},{"original":"524b4150ff92f1f4f8000024","majorVersion":0},{"original":"53e12043b82921000051cdf9","majorVersion":0,"config":{"slots":["eyes"],"programmableProperties":["pos","getEnemies"],"moreProgrammableProperties":["getItems","getFriends"]}},{"original":"524b75ad7fc0f6d519000001","majorVersion":0,"config":{"pos":{"x":33.230000000000004,"y":20.75,"z":2},"width":1,"height":2,"depth":1,"shape":"ellipsoid"}},{"original":"524b457bff92f1f4f8000031","majorVersion":0,"config":{"visualRange":50}}]}
describe 'InventoryView', -> #]
inventoryView = null #
#describe 'InventoryView', ->
beforeEach (done) -> # inventoryView = null
equipment = { 'feet':'boots', 'eyes': 'crude-glasses' } #
inventoryView = new InventoryView({ equipment: equipment }) # beforeEach (done) ->
responses = # equipment = { 'feet':'boots', 'eyes': 'crude-glasses' }
'/db/thang.type?view=items': thangTypes # inventoryView = new InventoryView({ equipment: equipment })
jasmine.Ajax.requests.sendResponses(responses) # responses =
_.defer -> # '/db/thang.type?view=items': thangTypes
inventoryView.render() # jasmine.Ajax.requests.sendResponses(responses)
done() # _.defer ->
# inventoryView.render()
it 'selects a slot when you click it', -> # done()
inventoryView.getSlot('eyes').click() #
expect(inventoryView.getSelectedSlot().data('slot')).toBe('eyes') # it 'selects a slot when you click it', ->
# inventoryView.getSlot('eyes').click()
it 'unselects a selected slot when you click it', -> # expect(inventoryView.getSelectedSlot().data('slot')).toBe('eyes')
inventoryView.getSlot('eyes').click().click() #
expect(inventoryView.getSelectedSlot().data('slot')).toBeUndefined() # it 'unselects a selected slot when you click it', ->
# inventoryView.getSlot('eyes').click().click()
it 'selects an available item when you click it', -> # expect(inventoryView.getSelectedSlot().data('slot')).toBeUndefined()
inventoryView.getAvailableItemContainer('boots-of-leaping').click() #
expect(inventoryView.getSelectedAvailableItemContainer().data('item-id')).toBe('boots-of-leaping') # it 'selects an available item when you click it', ->
# inventoryView.getAvailableItemContainer('boots-of-leaping').click()
it 'equips an available item when you double click it', -> # expect(inventoryView.getSelectedAvailableItemContainer().data('item-id')).toBe('boots-of-leaping')
inventoryView.getAvailableItemContainer('crossbow').click().dblclick() #
expect(inventoryView.getCurrentEquipmentConfig()['right-hand']).toBeTruthy() # it 'equips an available item when you double click it', ->
# inventoryView.getAvailableItemContainer('crossbow').click().dblclick()
it 'unequips an item when you double click it', -> # expect(inventoryView.getCurrentEquipmentConfig()['right-hand']).toBeTruthy()
inventoryView.getSlot('eyes').find('.item-view').click().dblclick() #
expect(inventoryView.getCurrentEquipmentConfig().eyes).toBeUndefined() # it 'unequips an item when you double click it', ->
# inventoryView.getSlot('eyes').find('.item-view').click().dblclick()
# expect(inventoryView.getCurrentEquipmentConfig().eyes).toBeUndefined()