Update campaign next level algorithm for practice levels

Don’t show not-started unlocked levels if previous incomplete practice
level is available
Yellow arrow points at adventurer levels too now

Closes #3882
This commit is contained in:
Matt Lott 2016-08-26 06:41:03 -07:00
parent 8bef580909
commit d20600b381
2 changed files with 112 additions and 62 deletions

View file

@ -178,7 +178,7 @@ module.exports = class CampaignView extends RootView
context.levels = _.reject context.levels, slug: reject context.levels = _.reject context.levels, slug: reject
if me.isOnFreeOnlyServer() if me.isOnFreeOnlyServer()
context.levels = _.reject context.levels, 'requiresSubscription' context.levels = _.reject context.levels, 'requiresSubscription'
@annotateLevel level for level in context.levels @annotateLevels(context.levels)
count = @countLevels context.levels count = @countLevels context.levels
context.levelsCompleted = count.completed context.levelsCompleted = count.completed
context.levelsTotal = count.total context.levelsTotal = count.total
@ -278,9 +278,11 @@ module.exports = class CampaignView extends RootView
return me.getCampaignAdsGroup() is 'leaderboard-ads' return me.getCampaignAdsGroup() is 'leaderboard-ads'
false false
annotateLevel: (level) -> annotateLevels: (orderedLevels) ->
previousIncompletePracticeLevel = false # Lock owned levels if there's a earlier incomplete practice level to play
for level in orderedLevels
level.position ?= { x: 10, y: 10 } level.position ?= { x: 10, y: 10 }
level.locked = not me.ownsLevel level.original level.locked = not me.ownsLevel(level.original) or previousIncompletePracticeLevel
level.locked = true if level.slug is 'kithgard-mastery' and @calculateExperienceScore() is 0 level.locked = true if level.slug is 'kithgard-mastery' and @calculateExperienceScore() is 0
level.locked = false if @levelStatusMap[level.slug] in ['started', 'complete'] level.locked = false if @levelStatusMap[level.slug] in ['started', 'complete']
level.locked = false if @editorMode level.locked = false if @editorMode
@ -290,10 +292,8 @@ module.exports = class CampaignView extends RootView
level.disabled = true if level.adminOnly and @levelStatusMap[level.slug] not in ['started', 'complete'] level.disabled = true if level.adminOnly and @levelStatusMap[level.slug] not in ['started', 'complete']
level.disabled = false if me.isInGodMode() level.disabled = false if me.isInGodMode()
level.color = 'rgb(255, 80, 60)' level.color = 'rgb(255, 80, 60)'
if level.requiresSubscription level.color = 'rgb(80, 130, 200)' if level.requiresSubscription
level.color = 'rgb(80, 130, 200)' level.color = 'rgb(200, 80, 200)' if level.adventurer
if level.adventurer
level.color = 'rgb(200, 80, 200)'
if unlocksHero = _.find(level.rewards, 'hero')?.hero if unlocksHero = _.find(level.rewards, 'hero')?.hero
level.unlocksHero = unlocksHero level.unlocksHero = unlocksHero
if level.unlocksHero if level.unlocksHero
@ -311,18 +311,22 @@ module.exports = class CampaignView extends RootView
""" """
level.color = 'rgb(80, 130, 200)' if problem.solved level.color = 'rgb(80, 130, 200)' if problem.solved
if level.practice and not level.locked and @levelStatusMap[level.slug] isnt 'complete' and
(not level.requiresSubscription or level.adventurer or not @requiresSubscription)
previousIncompletePracticeLevel = true
level.hidden = level.locked level.hidden = level.locked
if level.concepts?.length if level.concepts?.length
level.displayConcepts = level.concepts level.displayConcepts = level.concepts
maxConcepts = 6 maxConcepts = 6
if level.displayConcepts.length > maxConcepts if level.displayConcepts.length > maxConcepts
level.displayConcepts = level.displayConcepts.slice -maxConcepts level.displayConcepts = level.displayConcepts.slice -maxConcepts
level
countLevels: (levels) -> countLevels: (levels) ->
count = total: 0, completed: 0 count = total: 0, completed: 0
for level, levelIndex in levels for level, levelIndex in levels
@annotateLevel level unless level.locked? # Annotate if we haven't already. continue if level.practice
@annotateLevels(levels) unless level.locked? # Annotate if we haven't already.
unless level.disabled unless level.disabled
unlockedInSameCampaign = levelIndex < 5 # First few are always counted (probably unlocked in previous campaign) unlockedInSameCampaign = levelIndex < 5 # First few are always counted (probably unlocked in previous campaign)
for otherLevel in levels when not unlockedInSameCampaign and otherLevel isnt level for otherLevel in levels when not unlockedInSameCampaign and otherLevel isnt level
@ -336,34 +340,42 @@ module.exports = class CampaignView extends RootView
leaderboardModal = new LeaderboardModal supermodel: @supermodel, levelSlug: levelSlug leaderboardModal = new LeaderboardModal supermodel: @supermodel, levelSlug: levelSlug
@openModalView leaderboardModal @openModalView leaderboardModal
determineNextLevel: (levels) -> determineNextLevel: (orderedLevels) ->
foundNext = false
dontPointTo = ['lost-viking', 'kithgard-mastery'] # Challenge levels we don't want most players bashing heads against dontPointTo = ['lost-viking', 'kithgard-mastery'] # Challenge levels we don't want most players bashing heads against
subscriptionPrompts = [{slug: 'boom-and-bust', unless: 'defense-of-plainswood'}] subscriptionPrompts = [{slug: 'boom-and-bust', unless: 'defense-of-plainswood'}]
for level in levels
# Iterate through all levels in order and look to find the first unlocked one that meets all our criteria for being pointed out as the next level. findNextLevel = (nextLevels, practiceOnly) =>
level.nextLevels = (reward.level for reward in level.rewards ? [] when reward.level) for nextLevelOriginal in nextLevels
unless foundNext nextLevel = _.find orderedLevels, original: nextLevelOriginal
for nextLevelOriginal in level.nextLevels continue if not nextLevel or nextLevel.locked
nextLevel = _.find levels, original: nextLevelOriginal continue if practiceOnly and not nextLevel.practice
# If it's a challenge level, we efficiently determine whether we actually do want to point it out. # If it's a challenge level, we efficiently determine whether we actually do want to point it out.
if nextLevel and nextLevel.slug is 'kithgard-mastery' and not nextLevel.locked and not @levelStatusMap[nextLevel.slug] and @calculateExperienceScore() >= 3 if nextLevel.slug is 'kithgard-mastery' and not @levelStatusMap[nextLevel.slug] and @calculateExperienceScore() >= 3
unless (timesPointedOut = storage.load("pointed-out-#{nextLevel.slug}") or 0) > 3 unless (timesPointedOut = storage.load("pointed-out-#{nextLevel.slug}") or 0) > 3
# We may determineNextLevel more than once per render, so we can't just do this once. But we do give up after a couple highlights. # We may determineNextLevel more than once per render, so we can't just do this once. But we do give up after a couple highlights.
dontPointTo = _.without dontPointTo, nextLevel.slug dontPointTo = _.without dontPointTo, nextLevel.slug
storage.save "pointed-out-#{nextLevel.slug}", timesPointedOut + 1 storage.save "pointed-out-#{nextLevel.slug}", timesPointedOut + 1
# Should we point this level out? # Should we point this level out?
if nextLevel and not nextLevel.locked and not nextLevel.disabled and @levelStatusMap[nextLevel.slug] isnt 'complete' and nextLevel.slug not in dontPointTo and not nextLevel.replayable and ( if not nextLevel.disabled and @levelStatusMap[nextLevel.slug] isnt 'complete' and nextLevel.slug not in dontPointTo and
me.isPremium() or not nextLevel.requiresSubscription or not nextLevel.replayable and (
me.isPremium() or not nextLevel.requiresSubscription or nextLevel.adventurer or
_.any(subscriptionPrompts, (prompt) => nextLevel.slug is prompt.slug and not @levelStatusMap[prompt.unless]) _.any(subscriptionPrompts, (prompt) => nextLevel.slug is prompt.slug and not @levelStatusMap[prompt.unless])
) )
nextLevel.next = true nextLevel.next = true
foundNext = true return true
break false
if not foundNext and levels[0] and not levels[0].locked and @levelStatusMap[levels[0].slug] isnt 'complete'
levels[0].next = true foundNext = false
for level in orderedLevels
# Iterate through all levels in order and look to find the first unlocked one that meets all our criteria for being pointed out as the next level.
level.nextLevels = (reward.level for reward in level.rewards ? [] when reward.level)
break if foundNext = findNextLevel(level.nextLevels, true) # Check practice levels first
break if foundNext = findNextLevel(level.nextLevels, false)
if not foundNext and orderedLevels[0] and not orderedLevels[0].locked and @levelStatusMap[orderedLevels[0].slug] isnt 'complete'
orderedLevels[0].next = true
calculateExperienceScore: -> calculateExperienceScore: ->
adultPoint = me.get('ageRange') in ['18-24', '25-34', '35-44', '45-100'] # They have to have answered the poll for this, likely after Shadow Guard. adultPoint = me.get('ageRange') in ['18-24', '25-34', '35-44', '45-100'] # They have to have answered the poll for this, likely after Shadow Guard.
@ -411,6 +423,7 @@ module.exports = class CampaignView extends RootView
@particleMan.removeEmitters() @particleMan.removeEmitters()
@particleMan.attach @$el.find('.map') @particleMan.attach @$el.find('.map')
for level in @campaign.renderedLevels ? {} for level in @campaign.renderedLevels ? {}
continue if level.practice
terrain = @terrain.replace('-branching-test', '').replace(/(campaign-)?(game|web)-dev-\d/, 'forest').replace('intro', 'dungeon') terrain = @terrain.replace('-branching-test', '').replace(/(campaign-)?(game|web)-dev-\d/, 'forest').replace('intro', 'dungeon')
particleKey = ['level', terrain] particleKey = ['level', terrain]
particleKey.push level.type if level.type and not (level.type in ['hero', 'course']) # Would use isType, but it's not a Level model particleKey.push level.type if level.type and not (level.type in ['hero', 'course']) # Would use isType, but it's not a Level model

View file

@ -0,0 +1,37 @@
factories = require 'test/app/factories'
CampaignView = require 'views/play/CampaignView'
Levels = require 'collections/Levels'
describe 'CampaignView', ->
describe 'when 4 earned levels', ->
beforeEach ->
@campaignView = new CampaignView()
@campaignView.levelStatusMap = {}
levels = new Levels(_.times(4, -> factories.makeLevel()))
@campaignView.campaign = factories.makeCampaign({}, {levels})
@levels = (level.toJSON() for level in levels.models)
earned = me.get('earned') or {}
earned.levels ?= []
earned.levels.push(level.original) for level in @levels
me.set('earned', earned)
describe 'and 3rd one is practice', ->
beforeEach ->
@levels[2].practice = true
@campaignView.annotateLevels(@levels)
it 'hides next levels if there are practice levels to do', ->
expect(@levels[2].hidden).toEqual(false)
expect(@levels[3].hidden).toEqual(true)
describe 'and 2nd rewards a practice a non-practice level', ->
beforeEach ->
@campaignView.levelStatusMap[@levels[0].slug] = 'complete'
@campaignView.levelStatusMap[@levels[1].slug] = 'complete'
@levels[1].rewards = [{level: @levels[2].original}, {level: @levels[3].original}]
@levels[2].practice = true
@campaignView.annotateLevels(@levels)
@campaignView.determineNextLevel(@levels)
it 'points at practice level first', ->
expect(@levels[2].next).toEqual(true)
expect(@levels[3].next).not.toBeDefined(true)