mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-25 16:47:58 -05:00
Merge branch 'master' into production
This commit is contained in:
commit
6d0c30d69d
23 changed files with 171 additions and 33 deletions
BIN
app/assets/images/common/gem.png
Normal file
BIN
app/assets/images/common/gem.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.5 KiB |
|
@ -95,7 +95,7 @@
|
|||
|
||||
home:
|
||||
slogan: "Learn to Code by Playing a Game"
|
||||
no_ie: "CodeCombat does not run in Internet Explorer 9 or older. Sorry!"
|
||||
no_ie: "CodeCombat does not run in Internet Explorer 8 or older. Sorry!"
|
||||
no_mobile: "CodeCombat wasn't designed for mobile devices and may not work!"
|
||||
play: "Play" # The big play button that just starts playing a level
|
||||
old_browser: "Uh oh, your browser is too old to run CodeCombat. Sorry!"
|
||||
|
|
|
@ -64,3 +64,15 @@ module.exports = class User extends CocoModel
|
|||
|
||||
level: ->
|
||||
User.levelFromExp(@get('points'))
|
||||
|
||||
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)
|
||||
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 ? []
|
||||
|
|
|
@ -45,7 +45,7 @@ _.extend AchievementSchema.properties,
|
|||
query:
|
||||
#type:'object'
|
||||
$ref: '#/definitions/mongoFindQuery'
|
||||
worth: c.float
|
||||
worth: c.float()
|
||||
collection: {type: 'string'}
|
||||
description: c.shortString()
|
||||
userField: c.shortString()
|
||||
|
@ -61,7 +61,7 @@ _.extend AchievementSchema.properties,
|
|||
description: 'For repeatables only. Denotes the field a repeatable achievement needs for its calculations'
|
||||
recalculable:
|
||||
type: 'boolean'
|
||||
description: 'Needs to be set to true before it is elligible for recalculation.'
|
||||
description: 'Needs to be set to true before it is eligible for recalculation.'
|
||||
function:
|
||||
type: 'object'
|
||||
description: 'Function that gives total experience for X amount achieved'
|
||||
|
@ -82,6 +82,8 @@ _.extend AchievementSchema.properties,
|
|||
format: 'i18n'
|
||||
props: ['name', 'description']
|
||||
description: 'Help translate this achievement'
|
||||
rewards: c.RewardSchema 'awarded by this achievement'
|
||||
|
||||
|
||||
_.extend AchievementSchema, # Let's have these on the bottom
|
||||
# TODO We really need some required properties in my opinion but this makes creating new achievements impossible as it is now
|
||||
|
|
|
@ -5,7 +5,7 @@ module.exports =
|
|||
type: 'object'
|
||||
default:
|
||||
previouslyAchievedAmount: 0
|
||||
|
||||
|
||||
properties:
|
||||
user: c.objectId
|
||||
links:
|
||||
|
@ -30,4 +30,5 @@ module.exports =
|
|||
achievedAmount: type: 'number'
|
||||
earnedPoints: type: 'number'
|
||||
previouslyAchievedAmount: {type: 'number'}
|
||||
earnedRewards: c.RewardSchema 'awarded by this achievement to this user'
|
||||
notified: type: 'boolean'
|
||||
|
|
|
@ -17,6 +17,8 @@ UserSchema = c.object
|
|||
simulatedBy: 0
|
||||
simulatedFor: 0
|
||||
jobProfile: {}
|
||||
earned: {heroes: [], items: [], levels: [], gems: 0}
|
||||
purchased: {heroes: [], items: [], levels: [], gems: 0}
|
||||
|
||||
c.extendNamedProperties UserSchema # let's have the name be the first property
|
||||
|
||||
|
@ -265,6 +267,8 @@ _.extend UserSchema.properties,
|
|||
thangTypeTranslationPatches: c.int()
|
||||
thangTypeMiscPatches: c.int()
|
||||
|
||||
earned: c.RewardSchema 'earned by achievements'
|
||||
purchased: c.RewardSchema 'purchased with gems'
|
||||
|
||||
c.extendBasicProperties UserSchema, 'user'
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ me.pct = (ext) -> combine({type: 'number', maximum: 1.0, minimum: 0.0}, ext)
|
|||
me.date = (ext) -> combine({type: ['object', 'string'], format: 'date-time'}, ext)
|
||||
# should just be string (Mongo ID), but sometimes mongoose turns them into objects representing those, so we are lenient
|
||||
me.objectId = (ext) -> schema = combine({type: ['object', 'string']}, ext)
|
||||
me.stringID = (ext) -> schema = combine({type: 'string', minLength: 24, maxLength: 24}, ext)
|
||||
me.url = (ext) -> combine({type: 'string', format: 'url', pattern: urlPattern}, ext)
|
||||
me.int = (ext) -> combine {type: 'integer'}, ext
|
||||
me.float = (ext) -> combine {type: 'number'}, ext
|
||||
|
@ -209,3 +210,16 @@ me.HeroConfigSchema = me.object {description: 'Which hero the player is using, e
|
|||
description: 'The inventory of the hero: slots to item ThangTypes.'
|
||||
additionalProperties: me.objectId(description: 'An item ThangType.')
|
||||
thangType: me.objectId(links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], title: 'Thang Type', description: 'The ThangType of the hero.', format: 'thang-type')
|
||||
|
||||
me.RewardSchema = (descriptionFragment='earned by achievements') ->
|
||||
type: 'object'
|
||||
additionalProperties: false
|
||||
description: "Rewards #{descriptionFragment}."
|
||||
properties:
|
||||
heroes: me.array {uniqueItems: true, description: "Heroes #{descriptionFragment}."},
|
||||
me.stringID(links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], title: 'Hero ThangType', description: 'A reference to the earned hero ThangType.', format: 'thang-type')
|
||||
items: me.array {uniqueItems: true, description: "Items #{descriptionFragment}."},
|
||||
me.stringID(links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], title: 'Item ThangType', description: 'A reference to the earned item ThangType.', format: 'thang-type')
|
||||
levels: me.array {uniqueItems: true, description: "Levels #{descriptionFragment}."},
|
||||
me.stringID(links: [{rel: 'db', href: '/db/level/{($)}/version'}], title: 'Level', description: 'A reference to the earned Level.', format: 'latest-version-original-reference')
|
||||
gems: me.int {description: "Gems #{descriptionFragment}."}
|
||||
|
|
|
@ -314,3 +314,27 @@ kbd
|
|||
background-color: #333
|
||||
border-radius: 3px
|
||||
@include box-shadow(inset 0 -1px 0 rgba(0, 0, 0, .25))
|
||||
|
||||
.gem
|
||||
display: inline-block
|
||||
background: transparent url(/images/common/gem.png) no-repeat center
|
||||
background-size: contain
|
||||
width: 80px
|
||||
height: 80px
|
||||
margin: 0px 2px
|
||||
|
||||
&.gem-20
|
||||
width: 20px
|
||||
height: 20px
|
||||
|
||||
&.gem-25
|
||||
width: 25px
|
||||
height: 25px
|
||||
|
||||
&.gem-40
|
||||
width: 40px
|
||||
height: 40px
|
||||
|
||||
&.gem-60
|
||||
width: 60px
|
||||
height: 60px
|
||||
|
|
|
@ -52,9 +52,16 @@ $gameControlMargin: 30px
|
|||
border: 2px groove white
|
||||
@include transition(margin-bottom 0.5s ease)
|
||||
|
||||
&.disabled
|
||||
&.disabled, &.locked
|
||||
background-image: url(/images/pages/game-menu/lock.png)
|
||||
background-size: 75%
|
||||
background-repeat: no-repeat
|
||||
background-position: 50% 50%
|
||||
opacity: 0.7
|
||||
|
||||
a
|
||||
cursor: default
|
||||
|
||||
&.next
|
||||
width: 2 * $levelDotWidth
|
||||
height: 2 * $levelDotHeight
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
@import "bootstrap/variables"
|
||||
@import "bootstrap/mixins"
|
||||
@import "base"
|
||||
|
||||
#employers-wrapper
|
||||
background-color: #B4B4B4
|
||||
|
@ -51,5 +50,3 @@
|
|||
#login-button
|
||||
margin-left: 40%
|
||||
width: 20%
|
||||
|
||||
|
|
@ -2,23 +2,25 @@
|
|||
.carousel-indicator-container
|
||||
ol.carousel-indicators
|
||||
for hero, index in heroes
|
||||
- var info = heroInfo[hero.get('slug')]
|
||||
li(data-hero-id=hero.get('original'), title=hero.get('name'), data-slide-to=index, data-target="#hero-carousel", class="hero-indicator" + (info.status == "Locked" ? " locked" : ""))
|
||||
li(data-hero-id=hero.get('original'), title=hero.get('name'), data-slide-to=index, data-target="#hero-carousel", class="hero-indicator" + (hero.locked ? " locked" : ""))
|
||||
.hero-avatar
|
||||
if info.status == "Locked"
|
||||
if hero.locked
|
||||
img.lock-indicator(src="/images/pages/game-menu/lock.png")
|
||||
|
||||
.carousel-inner
|
||||
for hero in heroes
|
||||
- var info = heroInfo[hero.get('slug')]
|
||||
div(class="item hero-item" + (info.status == "Locked" ? " locked" : ""), data-hero-id=hero.get('original'))
|
||||
div(class="item hero-item" + (hero.locked ? " locked" : ""), data-hero-id=hero.get('original'))
|
||||
canvas.hero-canvas
|
||||
.hero-stats
|
||||
h2= info.fullName
|
||||
p
|
||||
span(data-i18n="choose_hero.status") Status
|
||||
span.spr :
|
||||
| #{info.status}
|
||||
if hero.locked
|
||||
| #{info.status}
|
||||
else
|
||||
| Available
|
||||
p
|
||||
span(data-i18n="choose_hero.weapons") Weapons
|
||||
span.spr :
|
||||
|
|
|
@ -4,8 +4,8 @@ block content
|
|||
|
||||
h1#site-slogan(data-i18n="home.slogan") Learn to Code by Playing a Game
|
||||
|
||||
.alert.alert-danger.lt-ie10
|
||||
strong(data-i18n="home.no_ie") CodeCombat does not run in Internet Explorer 9 or older. Sorry!
|
||||
.alert.alert-danger.lt-ie9
|
||||
strong(data-i18n="home.no_ie") CodeCombat does not run in Internet Explorer 8 or older. Sorry!
|
||||
|
||||
if isMobile
|
||||
.alert.alert-danger.mobile
|
||||
|
|
|
@ -6,12 +6,12 @@
|
|||
each level in campaign.levels
|
||||
- var next = !seenNext && levelStatusMap[level.id] != "complete";
|
||||
- seenNext = seenNext || next;
|
||||
div(style="left: #{level.x}%; bottom: #{level.y}%; background-color: #{campaign.color}", class="level" + (next ? " next" : "") + (level.disabled ? " disabled" : "") + " " + levelStatusMap[level.id] || "", data-level-id=level.id, title=level.name)
|
||||
div(style="left: #{level.x}%; bottom: #{level.y}%; background-color: #{campaign.color}", class="level" + (next ? " next" : "") + (level.disabled ? " disabled" : "") + (level.locked ? " locked" : "") + " " + levelStatusMap[level.id] || "", data-level-id=level.id, title=level.name)
|
||||
a(href=level.type == 'hero' ? '#' : level.disabled ? "/play" : "/play/#{level.levelPath || 'level'}/#{level.id}", disabled=level.disabled, data-level-id=level.id, data-level-path=level.levelPath || 'level', data-level-name=level.name)
|
||||
div(style="left: #{level.x}%; bottom: #{level.y}%", class="level-shadow" + (next ? " next" : "") + " " + levelStatusMap[level.id] || "")
|
||||
.level-info-container(data-level-id=level.id, data-level-path=level.levelPath || 'level', data-level-name=level.name)
|
||||
div(class="level-info " + (levelStatusMap[level.id] || ""))
|
||||
h3= level.name + (level.disabled ? " (Coming soon!)" : "")
|
||||
h3= level.name + (level.disabled ? " (Coming soon!)" : (level.locked ? " (Locked)" : ""))
|
||||
.level-description= level.description
|
||||
span(data-i18n="play.level_difficulty") Difficulty:
|
||||
each i in Array(level.difficulty)
|
||||
|
@ -46,6 +46,8 @@
|
|||
a(href="/play-old", data-i18n="play.older_campaigns").header-font Older Campaigns
|
||||
|
||||
.user-status.header-font
|
||||
span.gem.gem-20
|
||||
span.spr= me.gems()
|
||||
if me.get('anonymous')
|
||||
span.spr(data-i18n="play.anonymous_player") Anonymous Player
|
||||
button.btn.btn-default.btn-flat.btn-sm(data-toggle='coco-modal', data-target='modal/AuthModal', data-i18n="login.log_in")
|
||||
|
|
|
@ -360,11 +360,14 @@ class LatestVersionReferenceNode extends TreemaNode
|
|||
return 'Unknown' unless @settings.supermodel?
|
||||
m = CocoModel.getReferencedModel(@getData(), @workingSchema)
|
||||
data = @getData()
|
||||
m = @settings.supermodel.getModelByOriginalAndMajorVersion(m.constructor, data.original, data.majorVersion)
|
||||
if _.isString data # LatestVersionOriginalReferenceNode just uses original
|
||||
m = @settings.supermodel.getModelByOriginal(m.constructor, data)
|
||||
else
|
||||
m = @settings.supermodel.getModelByOriginalAndMajorVersion(m.constructor, data.original, data.majorVersion)
|
||||
if @instance and not m
|
||||
m = @instance
|
||||
@settings.supermodel.registerModel(m)
|
||||
return 'Unknown' unless m
|
||||
return 'Unknown - ' + (data.original ? data) unless m
|
||||
return @modelToString(m)
|
||||
|
||||
saveChanges: ->
|
||||
|
@ -409,6 +412,15 @@ class LatestVersionReferenceNode extends TreemaNode
|
|||
selected = @getSelectedResultEl()
|
||||
return not selected.length
|
||||
|
||||
class LatestVersionOriginalReferenceNode extends LatestVersionReferenceNode
|
||||
# Just for saving the original, not the major version.
|
||||
saveChanges: ->
|
||||
selected = @getSelectedResultEl()
|
||||
return unless selected.length
|
||||
fullValue = selected.data('value')
|
||||
@data = fullValue.attributes.original
|
||||
@instance = fullValue
|
||||
|
||||
class LevelComponentReferenceNode extends LatestVersionReferenceNode
|
||||
# HACK: this list of properties is needed by the thang components edit view and config views.
|
||||
# need a better way to specify this, or keep the search models from bleeding into those
|
||||
|
@ -436,6 +448,7 @@ module.exports.setup = ->
|
|||
TreemaNode.setNodeSubclass('javascript', JavaScriptTreema)
|
||||
TreemaNode.setNodeSubclass('image-file', ImageFileTreema)
|
||||
TreemaNode.setNodeSubclass('latest-version-reference', LatestVersionReferenceNode)
|
||||
TreemaNode.setNodeSubclass('latest-version-original-reference', LatestVersionOriginalReferenceNode)
|
||||
TreemaNode.setNodeSubclass('component-reference', LevelComponentReferenceNode)
|
||||
TreemaNode.setNodeSubclass('i18n', InternationalizationNode)
|
||||
TreemaNode.setNodeSubclass('sound-file', SoundFileTreema)
|
||||
|
|
|
@ -15,8 +15,6 @@ module.exports = class AchievementPopup extends CocoView
|
|||
@popup ?= true
|
||||
@className += ' popup' if @popup
|
||||
super options
|
||||
console.debug 'Created an AchievementPopup', @$el
|
||||
|
||||
@render()
|
||||
|
||||
calculateData: ->
|
||||
|
@ -62,7 +60,6 @@ module.exports = class AchievementPopup extends CocoView
|
|||
c
|
||||
|
||||
render: ->
|
||||
console.debug 'render achievement popup'
|
||||
super()
|
||||
@container.prepend @$el
|
||||
if @popup
|
||||
|
|
|
@ -5,6 +5,7 @@ AchievementPopup = require 'views/achievements/AchievementPopup'
|
|||
ConfirmModal = require 'views/modal/ConfirmModal'
|
||||
errors = require 'lib/errors'
|
||||
app = require 'application'
|
||||
nodes = require 'views/editor/level/treema_nodes'
|
||||
|
||||
module.exports = class AchievementEditView extends RootView
|
||||
id: 'editor-achievement-edit-view'
|
||||
|
@ -36,8 +37,13 @@ module.exports = class AchievementEditView extends RootView
|
|||
readOnly: me.get('anonymous')
|
||||
callbacks:
|
||||
change: @pushChangesToPreview
|
||||
nodeClasses:
|
||||
'thang-type': nodes.ThangTypeNode
|
||||
'item-thang-type': nodes.ItemThangTypeNode
|
||||
supermodel: @supermodel
|
||||
@treema = @$el.find('#achievement-treema').treema(options)
|
||||
@treema.build()
|
||||
@treema.childrenTreemas.rewards?.open(3)
|
||||
@pushChangesToPreview()
|
||||
|
||||
getRenderData: (context={}) ->
|
||||
|
|
|
@ -36,6 +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
|
||||
context.level = @options.level
|
||||
context.codeLanguages = [
|
||||
{id: 'python', name: 'Python'}
|
||||
|
@ -76,7 +77,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'
|
||||
locked = heroInfo.status is 'Locked' and not me.earnedHero ThangType.heroes[hero.get('slug')]
|
||||
hero = @loadHero hero, heroIndex
|
||||
@preloadHero heroIndex + 1
|
||||
@preloadHero heroIndex - 1
|
||||
|
|
|
@ -321,6 +321,8 @@ module.exports = class InventoryView extends CocoView
|
|||
for slot, item of items
|
||||
@allowedItems.push gear[item] unless gear[item] in @allowedItems
|
||||
break if level is @options.levelID
|
||||
for item in me.get('earned')?.items ? [] when not (item in @allowedItems)
|
||||
@allowedItems.push item
|
||||
|
||||
onHeroSelectionUpdated: (e) ->
|
||||
@selectedHero = e.hero
|
||||
|
|
|
@ -65,9 +65,10 @@ module.exports = class WorldMapView extends RootView
|
|||
context = super(context)
|
||||
context.campaigns = campaigns
|
||||
for campaign in context.campaigns
|
||||
for level in campaign.levels
|
||||
for level, index in campaign.levels
|
||||
level.x ?= 10 + 80 * Math.random()
|
||||
level.y ?= 10 + 80 * Math.random()
|
||||
level.locked = index > 0 and not me.earnedLevel level.original
|
||||
context.levelStatusMap = @levelStatusMap
|
||||
context.levelPlayCountMap = @levelPlayCountMap
|
||||
context.isIPadApp = application.isIPadApp
|
||||
|
@ -96,7 +97,7 @@ module.exports = class WorldMapView extends RootView
|
|||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
@$levelInfo?.hide()
|
||||
return if $(e.target).attr('disabled')
|
||||
return if $(e.target).attr('disabled') or $(e.target).parent().hasClass 'locked'
|
||||
if application.isIPadApp
|
||||
levelID = $(e.target).parents('.level').data('level-id')
|
||||
@$levelInfo = @$el.find(".level-info-container[data-level-id=#{levelID}]").show()
|
||||
|
@ -515,6 +516,7 @@ hero = [
|
|||
type: 'hero'
|
||||
difficulty: 1
|
||||
id: 'dungeons-of-kithgard'
|
||||
original: '528110f30268d018e3000001'
|
||||
description: 'Grab the gem, but touch nothing else. Start here.'
|
||||
x: 17.23
|
||||
y: 36.94
|
||||
|
@ -524,6 +526,7 @@ hero = [
|
|||
type: 'hero'
|
||||
difficulty: 1
|
||||
id: 'gems-in-the-deep'
|
||||
original: '54173c90844506ae0195a0b4'
|
||||
description: 'Quickly collect the gems; you will need them.'
|
||||
x: 22.6
|
||||
y: 35.1
|
||||
|
@ -533,6 +536,7 @@ hero = [
|
|||
type: 'hero'
|
||||
difficulty: 1
|
||||
id: 'shadow-guard'
|
||||
original: '54174347844506ae0195a0b8'
|
||||
description: 'Evade the Kithgard minion.'
|
||||
x: 27.74
|
||||
y: 35.17
|
||||
|
@ -542,6 +546,7 @@ hero = [
|
|||
type: 'hero'
|
||||
difficulty: 1
|
||||
id: 'true-names'
|
||||
original: '541875da4c16460000ab990f'
|
||||
description: 'Learn an enemy\'s true name to defeat it.'
|
||||
x: 32.7
|
||||
y: 36.7
|
||||
|
@ -551,6 +556,7 @@ hero = [
|
|||
type: 'hero'
|
||||
difficulty: 1
|
||||
id: 'the-raised-sword'
|
||||
original: '5418aec24c16460000ab9aa6'
|
||||
description: 'Learn to equip yourself for combat.'
|
||||
x: 36.6
|
||||
y: 39.5
|
||||
|
@ -560,6 +566,7 @@ hero = [
|
|||
type: 'hero'
|
||||
difficulty: 1
|
||||
id: 'the-first-kithmaze'
|
||||
original: '5418b9d64c16460000ab9ab4'
|
||||
description: 'The builders of Kith constructed many mazes to confuse travelers.'
|
||||
x: 38.4
|
||||
y: 43.5
|
||||
|
@ -569,6 +576,7 @@ hero = [
|
|||
type: 'hero'
|
||||
difficulty: 1
|
||||
id: 'the-second-kithmaze'
|
||||
original: '5418cf256bae62f707c7e1c3'
|
||||
description: 'Many have tried, few have found their way through this maze.'
|
||||
x: 38.9
|
||||
y: 48.1
|
||||
|
@ -578,6 +586,7 @@ hero = [
|
|||
type: 'hero'
|
||||
difficulty: 1
|
||||
id: 'new-sight'
|
||||
original: '5418d40f4c16460000ab9ac2'
|
||||
description: 'A true name can only be seen with the correct lenses.'
|
||||
x: 39.3
|
||||
y: 53.1
|
||||
|
@ -587,6 +596,7 @@ hero = [
|
|||
type: 'hero'
|
||||
difficulty: 1
|
||||
id: 'lowly-kithmen'
|
||||
original: '541b24511ccc8eaae19f3c1f'
|
||||
description: 'Use your glasses to seek out and attack the Kithmen.'
|
||||
x: 39.4
|
||||
y: 57.7
|
||||
|
@ -596,6 +606,7 @@ hero = [
|
|||
type: 'hero'
|
||||
difficulty: 1
|
||||
id: 'a-bolt-in-the-dark'
|
||||
original: '541b288e1ccc8eaae19f3c25'
|
||||
description: 'Kithmen are not the only ones to stand in your way.'
|
||||
x: 40.0
|
||||
y: 63.2
|
||||
|
@ -605,6 +616,7 @@ hero = [
|
|||
type: 'hero'
|
||||
difficulty: 1
|
||||
id: 'the-final-kithmaze'
|
||||
original: '541b434e1ccc8eaae19f3c33'
|
||||
description: 'To escape you must find your way through an Elder Kithman\'s maze.'
|
||||
x: 42.67
|
||||
y: 67.98
|
||||
|
@ -614,6 +626,7 @@ hero = [
|
|||
type: 'hero'
|
||||
difficulty: 1
|
||||
id: 'kithgard-gates'
|
||||
original: '541c9a30c6362edfb0f34479'
|
||||
description: 'Escape the Kithgard dungeons and don\'t let the guardians get you.'
|
||||
x: 47.38
|
||||
y: 70.55
|
||||
|
@ -624,6 +637,7 @@ hero = [
|
|||
type: 'hero'
|
||||
difficulty: 1
|
||||
id: 'defence-of-plainswood'
|
||||
original: '541b67f71ccc8eaae19f3c62'
|
||||
description: 'Protect the peasants from the pursuing ogres.'
|
||||
x: 52.66
|
||||
y: 69.66
|
||||
|
|
|
@ -75,7 +75,7 @@
|
|||
"javascript-brunch": "> 1.0 < 1.8",
|
||||
"coffee-script-brunch": "https://github.com/brunch/coffee-script-brunch/tarball/master",
|
||||
"coffeelint-brunch": "> 1.0 < 1.8",
|
||||
"sass-brunch": "1.7.0",
|
||||
"sass-brunch": "1.8.3",
|
||||
"css-brunch": "> 1.0 < 1.8",
|
||||
"jade-brunch": "> 1.0 < 1.8",
|
||||
"uglify-js-brunch": "~1.7.4",
|
||||
|
@ -86,7 +86,7 @@
|
|||
"marked": "0.2.x",
|
||||
"telepath-brunch": "https://github.com/nwinter/telepath-brunch/tarball/master",
|
||||
"bower": "~1.3.8",
|
||||
"bless-brunch": "~1.6.1",
|
||||
"bless-brunch": "https://github.com/ThomasConner/bless-brunch/tarball/master",
|
||||
"karma-script-launcher": "~0.1.0",
|
||||
"karma-chrome-launcher": "~0.1.2",
|
||||
"karma-firefox-launcher": "~0.1.3",
|
||||
|
|
|
@ -5,7 +5,7 @@ class AchievementHandler extends Handler
|
|||
modelClass: Achievement
|
||||
|
||||
# Used to determine which properties requests may edit
|
||||
editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon', 'function', 'related', 'difficulty', 'category', 'recalculable']
|
||||
editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon', 'function', 'related', 'difficulty', 'category', 'recalculable', 'rewards']
|
||||
allowedMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE']
|
||||
jsonSchema = require '../../app/schemas/models/achievement.coffee'
|
||||
|
||||
|
|
|
@ -77,15 +77,21 @@ class EarnedAchievementHandler extends Handler
|
|||
EarnedAchievement.find {user: userID}, (err, alreadyEarned) ->
|
||||
alreadyEarnedIDs = []
|
||||
previousPoints = 0
|
||||
previousRewards = heroes: [], items: [], levels: [], gems: 0
|
||||
async.each alreadyEarned, ((earned, doneWithEarned) ->
|
||||
if (_.find achievements, (single) -> earned.get('achievement') is single.get('_id').toHexString()) # if already earned
|
||||
alreadyEarnedIDs.push earned.get('achievement')
|
||||
previousPoints += earned.get 'earnedPoints'
|
||||
for rewardType in ['heroes', 'items', 'levels']
|
||||
previousRewards[rewardType] = previousRewards[rewardType].concat(earned.get('earnedRewards')?[rewardType] ? [])
|
||||
previousRewards.gems += earned.get('earnedRewards')?.gems ? 0
|
||||
doneWithEarned()
|
||||
), -> # After checking already achieved
|
||||
), (err) -> # After checking already achieved
|
||||
log.error err if err
|
||||
# TODO maybe also delete earned? Make sure you don't delete too many
|
||||
|
||||
newTotalPoints = 0
|
||||
newTotalRewards = heroes: [], items: [], levels: [], gems: 0
|
||||
|
||||
async.each achievements, ((achievement, doneWithAchievement) ->
|
||||
return doneWithAchievement() unless achievement.isRecalculable()
|
||||
|
@ -122,17 +128,43 @@ class EarnedAchievementHandler extends Handler
|
|||
earned.earnedPoints = newPoints
|
||||
newTotalPoints += newPoints
|
||||
|
||||
earned.earnedRewards = achievement.get('rewards')
|
||||
for rewardType in ['heroes', 'items', 'levels']
|
||||
newTotalRewards[rewardType] = newTotalRewards[rewardType].concat(achievement.get('rewards')?[rewardType] ? [])
|
||||
newTotalRewards.gems += achievement.get('rewards')?.gems ? 0
|
||||
|
||||
EarnedAchievement.update {achievement:earned.achievement, user:earned.user}, earned, {upsert: true}, (err) ->
|
||||
doneWithAchievement err
|
||||
), -> # Wrap up a user, save points
|
||||
), (err) -> # Wrap up a user, save points
|
||||
log.error err if err
|
||||
# Since some achievements cannot be recalculated it's important to deduct the old amount of exp
|
||||
# and add the new amount, instead of just setting to the new amount
|
||||
return doneWithUser(user) unless newTotalPoints
|
||||
#console.log 'User', user.get('name'), 'had newTotalPoints', newTotalPoints, 'and newTotalRewards', newTotalRewards
|
||||
return doneWithUser(user) unless newTotalPoints or newTotalRewards.gems or _.some(newTotalRewards, (r) -> r.length)
|
||||
# log.debug "Matched a total of #{newTotalPoints} new points"
|
||||
# log.debug "Incrementing score for these achievements with #{newTotalPoints - previousPoints}"
|
||||
pctDone = (100 * usersFinished / total).toFixed(2)
|
||||
console.log "Updated points to #{newTotalPoints}(+#{newTotalPoints - previousPoints}) for #{user.get('name') or '???'} (#{user.get('_id')}) (#{pctDone}%)"
|
||||
User.update {_id: userID}, {$inc: points: newTotalPoints - previousPoints}, {}, (err) ->
|
||||
update = {$inc: {points: newTotalPoints - previousPoints}}
|
||||
for rewardType, rewards of newTotalRewards
|
||||
if rewardType is 'gems'
|
||||
update.$inc['earned.gems'] = rewards - previousRewards.gems
|
||||
else
|
||||
previousCounts = _.countBy previousRewards[rewardType]
|
||||
newCounts = _.countBy rewards
|
||||
relevantRewards = _.union _.keys(previousCounts), _.keys(newCounts)
|
||||
for reward in relevantRewards
|
||||
[previousCount, newCount] = [previousCounts[reward], newCounts[reward]]
|
||||
if newCount and not previousCount
|
||||
update.$addToSet ?= {}
|
||||
update.$addToSet["earned.#{rewardType}"] ?= {$each: []}
|
||||
update.$addToSet["earned.#{rewardType}"].$each.push reward
|
||||
else if previousCount and not newCount
|
||||
# Might $pull $each also work here?
|
||||
update.$pullAll ?= {}
|
||||
update.$pullAll["earned.#{rewardType}"] ?= []
|
||||
update.$pullAll["earned.#{rewardType}"].push reward
|
||||
User.update {_id: userID}, update, {}, (err) ->
|
||||
log.error err if err?
|
||||
doneWithUser(user)
|
||||
|
||||
|
|
|
@ -50,12 +50,20 @@ AchievablePlugin = (schema, options) ->
|
|||
user: userID
|
||||
achievement: achievement._id.toHexString()
|
||||
achievementName: achievement.get 'name'
|
||||
earnedRewarsd: achievement.get 'rewards'
|
||||
|
||||
worth = achievement.get('worth') ? 10
|
||||
earnedPoints = 0
|
||||
wrapUp = ->
|
||||
# Update user's experience points
|
||||
User.update {_id: userID}, {$inc: {points: earnedPoints}}, {}, (err, count) ->
|
||||
update = {$inc: {points: earnedPoints}}
|
||||
for rewardType, rewards of achievement.get('rewards') ? {}
|
||||
if rewardType is 'gems'
|
||||
update.$inc['earned.gems'] = rewards if rewards
|
||||
else if rewards.length
|
||||
update.$addToSet ?= {}
|
||||
update.$addToSet["earned.#{rewardType}"] = $each: rewards
|
||||
User.update {_id: userID}, update, {}, (err, count) ->
|
||||
log.error err if err?
|
||||
|
||||
if isRepeatable
|
||||
|
|
Loading…
Reference in a new issue