From c50b443eca4449b9ea82dc640294f1fd7d29c2ed Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Thu, 25 Sep 2014 16:17:29 -0700 Subject: [PATCH 1/2] IE9 compat: css fixes Linking to bless-brunch git for fixes that blocked bless from splitting app.css into multiple files to address IE9 limits. sass-brunch version update fixes problems during the css file-splitting that were messing up nested @media queries. --- app/locale/en.coffee | 2 +- app/templates/home.jade | 4 ++-- package.json | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 2dcbae4c3..a053a90d0 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -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!" diff --git a/app/templates/home.jade b/app/templates/home.jade index 4a4ab5426..119032578 100644 --- a/app/templates/home.jade +++ b/app/templates/home.jade @@ -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 diff --git a/package.json b/package.json index 95d305bcd..d5a56c0af 100644 --- a/package.json +++ b/package.json @@ -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", From 98fed4a27786de79d56f5c072302c4ea49f0d022 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Fri, 26 Sep 2014 02:28:54 -0700 Subject: [PATCH 2/2] Extended Achievements to support earning heroes, items, levels, and gems. Fixed a few Achievement bugs. --- app/assets/images/common/gem.png | Bin 0 -> 7634 bytes app/models/User.coffee | 12 ++++++ app/schemas/models/achievement.coffee | 6 ++- app/schemas/models/earned_achievement.coffee | 3 +- app/schemas/models/user.coffee | 4 ++ app/schemas/schemas.coffee | 14 ++++++ app/styles/base.sass | 24 +++++++++++ app/styles/play/world-map-view.sass | 9 +++- app/styles/recruitment_base.sass | 3 -- app/templates/game-menu/choose-hero-view.jade | 12 +++--- app/templates/play/world-map-view.jade | 6 ++- app/treema-ext.coffee | 17 +++++++- .../achievements/AchievementPopup.coffee | 3 -- .../achievement/AchievementEditView.coffee | 6 +++ app/views/game-menu/ChooseHeroView.coffee | 3 +- app/views/game-menu/InventoryView.coffee | 2 + app/views/play/WorldMapView.coffee | 18 +++++++- .../achievements/achievement_handler.coffee | 2 +- .../earned_achievement_handler.coffee | 40 ++++++++++++++++-- server/plugins/achievements.coffee | 10 ++++- 20 files changed, 166 insertions(+), 28 deletions(-) create mode 100644 app/assets/images/common/gem.png diff --git a/app/assets/images/common/gem.png b/app/assets/images/common/gem.png new file mode 100644 index 0000000000000000000000000000000000000000..9598f6befb1785664ed1fabe2f7f5d4ba11f07f5 GIT binary patch literal 7634 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%0DVoe{J`CO;a>1kgGr@zU1?Pbt-_}T->v$o$fYef% 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 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 ? [] diff --git a/app/schemas/models/achievement.coffee b/app/schemas/models/achievement.coffee index 37690cadf..d364c0277 100644 --- a/app/schemas/models/achievement.coffee +++ b/app/schemas/models/achievement.coffee @@ -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 diff --git a/app/schemas/models/earned_achievement.coffee b/app/schemas/models/earned_achievement.coffee index 04cfe9b91..4b1848c3e 100644 --- a/app/schemas/models/earned_achievement.coffee +++ b/app/schemas/models/earned_achievement.coffee @@ -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' diff --git a/app/schemas/models/user.coffee b/app/schemas/models/user.coffee index 9901c5d76..11e9c9716 100644 --- a/app/schemas/models/user.coffee +++ b/app/schemas/models/user.coffee @@ -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' diff --git a/app/schemas/schemas.coffee b/app/schemas/schemas.coffee index da768fff0..5d30ac865 100644 --- a/app/schemas/schemas.coffee +++ b/app/schemas/schemas.coffee @@ -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}."} diff --git a/app/styles/base.sass b/app/styles/base.sass index 0522b50a1..2305f4f38 100644 --- a/app/styles/base.sass +++ b/app/styles/base.sass @@ -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 diff --git a/app/styles/play/world-map-view.sass b/app/styles/play/world-map-view.sass index c15c5ba69..5252b57a0 100644 --- a/app/styles/play/world-map-view.sass +++ b/app/styles/play/world-map-view.sass @@ -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 diff --git a/app/styles/recruitment_base.sass b/app/styles/recruitment_base.sass index f6c6e1062..1846007d3 100644 --- a/app/styles/recruitment_base.sass +++ b/app/styles/recruitment_base.sass @@ -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% - - \ No newline at end of file diff --git a/app/templates/game-menu/choose-hero-view.jade b/app/templates/game-menu/choose-hero-view.jade index 1e5fe4f4e..829b9ab27 100644 --- a/app/templates/game-menu/choose-hero-view.jade +++ b/app/templates/game-menu/choose-hero-view.jade @@ -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 : diff --git a/app/templates/play/world-map-view.jade b/app/templates/play/world-map-view.jade index b6aa7f806..88523f2e4 100644 --- a/app/templates/play/world-map-view.jade +++ b/app/templates/play/world-map-view.jade @@ -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") diff --git a/app/treema-ext.coffee b/app/treema-ext.coffee index 81020adac..8fe5242cc 100644 --- a/app/treema-ext.coffee +++ b/app/treema-ext.coffee @@ -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) diff --git a/app/views/achievements/AchievementPopup.coffee b/app/views/achievements/AchievementPopup.coffee index f25d9e88c..31356abbc 100644 --- a/app/views/achievements/AchievementPopup.coffee +++ b/app/views/achievements/AchievementPopup.coffee @@ -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 diff --git a/app/views/editor/achievement/AchievementEditView.coffee b/app/views/editor/achievement/AchievementEditView.coffee index cf50026a5..fbe1a60f1 100644 --- a/app/views/editor/achievement/AchievementEditView.coffee +++ b/app/views/editor/achievement/AchievementEditView.coffee @@ -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={}) -> diff --git a/app/views/game-menu/ChooseHeroView.coffee b/app/views/game-menu/ChooseHeroView.coffee index 11fdc8e4d..9903c00f1 100644 --- a/app/views/game-menu/ChooseHeroView.coffee +++ b/app/views/game-menu/ChooseHeroView.coffee @@ -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 diff --git a/app/views/game-menu/InventoryView.coffee b/app/views/game-menu/InventoryView.coffee index c2032813a..a406a8313 100644 --- a/app/views/game-menu/InventoryView.coffee +++ b/app/views/game-menu/InventoryView.coffee @@ -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 diff --git a/app/views/play/WorldMapView.coffee b/app/views/play/WorldMapView.coffee index cc333cbe3..99511e68c 100644 --- a/app/views/play/WorldMapView.coffee +++ b/app/views/play/WorldMapView.coffee @@ -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 diff --git a/server/achievements/achievement_handler.coffee b/server/achievements/achievement_handler.coffee index 3b9833836..7aed663ea 100644 --- a/server/achievements/achievement_handler.coffee +++ b/server/achievements/achievement_handler.coffee @@ -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' diff --git a/server/achievements/earned_achievement_handler.coffee b/server/achievements/earned_achievement_handler.coffee index 75359b78a..b61e05b1e 100644 --- a/server/achievements/earned_achievement_handler.coffee +++ b/server/achievements/earned_achievement_handler.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) diff --git a/server/plugins/achievements.coffee b/server/plugins/achievements.coffee index 040ecd2db..c0dacf96f 100644 --- a/server/plugins/achievements.coffee +++ b/server/plugins/achievements.coffee @@ -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