This commit is contained in:
George Saines 2014-11-12 16:48:10 -08:00
commit 50b4e79c7b
25 changed files with 329 additions and 82 deletions

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

@ -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
@ -261,6 +260,29 @@ module.exports = class ThangType extends CocoModel
createjs.Ticker.removeEventListener 'tick', @tick createjs.Ticker.removeEventListener 'tick', @tick
@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()
@ -339,9 +361,10 @@ module.exports = class ThangType extends CocoModel
unless itemConfig = _.find(components, original: LevelComponent.ItemID)?.config unless itemConfig = _.find(components, original: LevelComponent.ItemID)?.config
console.warn @get('name'), 'is not an item, but you are asking for its stats.' console.warn @get('name'), 'is not an item, but you are asking for its stats.'
return props: [], stats: {} return props: [], stats: {}
stats = {}
props = itemConfig.programmableProperties ? [] props = itemConfig.programmableProperties ? []
props = props.concat itemConfig.moreProgrammableProperties ? [] props = props.concat itemConfig.moreProgrammableProperties ? []
stats = {} props = _.without props, 'canCast', 'spellNames', 'spells'
for stat, modifiers of itemConfig.stats ? {} for stat, modifiers of itemConfig.stats ? {}
stats[stat] = @formatStatDisplay stat, modifiers stats[stat] = @formatStatDisplay stat, modifiers
for stat in itemConfig.extraHUDProperties ? [] for stat in itemConfig.extraHUDProperties ? []

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

@ -194,6 +194,26 @@ $gameControlMargin: 30px
display: block display: block
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

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

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

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

@ -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) ->
@ -45,8 +51,10 @@ module.exports = class BuyGemsModal extends ModalView
@$el.find('.modal-body').append($('<div class="alert alert-danger">Not implemented</div>')) @$el.find('.modal-body').append($('<div class="alert alert-danger">Not implemented</div>'))
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

@ -12,41 +12,47 @@ module.exports = class ItemDetailsView extends CocoView
constructor: -> constructor: ->
super(arguments...) super(arguments...)
@propDocs = {} @propDocs = {}
@spellDocs = {}
setItem: (@item) -> setItem: (@item) ->
if @item if @item
@spellDocs = {}
@item.name = utils.i18n @item.attributes, 'name' @item.name = utils.i18n @item.attributes, 'name'
@item.affordable = me.gems() >= @item.get('gems') @item.affordable = me.gems() >= @item.get('gems')
@item.owned = me.ownsItem @item.get('original') @item.owned = me.ownsItem @item.get('original')
@item.comingSoon = not @item.getFrontFacingStats().props.length and not _.size @item.getFrontFacingStats().stats # Temp: while there are placeholder items @item.comingSoon = not @item.getFrontFacingStats().props.length and not _.size @item.getFrontFacingStats().stats # Temp: while there are placeholder items
stats = @item.getFrontFacingStats() stats = @item.getFrontFacingStats()
props = (p for p in stats.props when not @propDocs[p]) props = (p for p in stats.props when not @propDocs[p])
if props.length > 0 if props.length > 0 or ('cast' in stats.props)
docs = new CocoCollection([], { docs = new CocoCollection([], {
url: '/db/level.component?view=prop-doc-lookup' url: '/db/level.component?view=prop-doc-lookup'
model: LevelComponent model: LevelComponent
project: [ project: [
'name'
'propertyDocumentation.name' 'propertyDocumentation.name'
'propertyDocumentation.description' 'propertyDocumentation.description'
'propertyDocumentation.i18n' 'propertyDocumentation.i18n'
] ]
}) })
docs.fetch({ data: { docs.fetch({ data: {
componentOriginals: [c.original for c in @item.get('components')].join(',') componentOriginals: [c.original for c in @item.get('components')].join(',')
propertyNames: props.join(',') propertyNames: props.join(',')
}}) }})
@listenToOnce docs, 'sync', @onDocsLoaded @listenToOnce docs, 'sync', @onDocsLoaded
@render() @render()
@$el.find('.nano:visible').nanoScroller() @$el.find('.nano:visible').nanoScroller()
onDocsLoaded: (levelComponents) -> onDocsLoaded: (levelComponents) ->
for component in levelComponents.models for component in levelComponents.models
for propDoc in component.get('propertyDocumentation') for propDoc in component.get('propertyDocumentation')
@propDocs[propDoc.name] = propDoc if /^cast.+/.test propDoc.name
@spellDocs[propDoc.name] = propDoc
else
@propDocs[propDoc.name] = propDoc
@render() @render()
getRenderData: -> getRenderData: ->
@ -57,20 +63,27 @@ module.exports = class ItemDetailsView extends CocoView
c.stats = _.values(stats.stats) c.stats = _.values(stats.stats)
_.last(c.stats).isLast = true if c.stats.length _.last(c.stats).isLast = true if c.stats.length
c.props = [] c.props = []
progLang = (me.get('aceConfig') ? {}).language or 'python' stats.props = stats.props.concat _.keys @spellDocs
codeLanguage = (me.get('aceConfig') ? {}).language or 'python'
for prop in stats.props for prop in stats.props
description = utils.i18n @propDocs[prop] ? {}, 'description' doc = @propDocs[prop] ? @spellDocs[prop] ? {}
description = utils.i18n doc, 'description'
if _.isObject description if _.isObject description
description = description[progLang] or _.values(description)[0] description = description[codeLanguage] or _.values(description)[0]
if _.isString description if _.isString description
description = description.replace(/#{spriteName}/g, 'hero') description = description.replace(/#{spriteName}/g, 'hero')
if fact = stats.stats.shieldDefenseFactor if fact = stats.stats.shieldDefenseFactor
description = description.replace(/#{shieldDefensePercent}%/g, fact.display) description = description.replace(/#{shieldDefensePercent}%/g, fact.display)
## We don't have the full components loaded here, so we can't really get most of these values.
#description = description.replace /#{([^.]+?)}/g, (match, keyChain) ->
# console.log 'gotta find', keyChain, 'from', match, 'and have', stats
# match
description = description.replace(/#{(.+?)}/g, '`$1`')
description = $(marked(description)).html() description = $(marked(description)).html()
c.props.push { c.props.push {
name: prop name: _.string.humanize prop
description: description or '...' description: description or '...'
} }
c c

View file

@ -37,9 +37,12 @@ LevelComponentHandler = class LevelComponentHandler extends Handler
properties = req.query.propertyNames.split(',') properties = req.query.propertyNames.split(',')
catch e catch e
return @sendBadInputError(res, 'Could not parse componentOriginals or propertyNames.') return @sendBadInputError(res, 'Could not parse componentOriginals or propertyNames.')
query['original'] = {$in: components} query['original'] = {$in: components}
query['propertyDocumentation.name'] = {$in: properties} query.$or = [
{'propertyDocumentation.name': {$in: properties}}
{'propertyDocumentation.name': {$regex: /^cast.+/}}
]
q = LevelComponent.find(query, projection) q = LevelComponent.find(query, projection)
q.exec (err, documents) => q.exec (err, documents) =>

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