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,51 +278,55 @@ module.exports = class CampaignView extends RootView
return me.getCampaignAdsGroup() is 'leaderboard-ads' return me.getCampaignAdsGroup() is 'leaderboard-ads'
false false
annotateLevel: (level) -> annotateLevels: (orderedLevels) ->
level.position ?= { x: 10, y: 10 } previousIncompletePracticeLevel = false # Lock owned levels if there's a earlier incomplete practice level to play
level.locked = not me.ownsLevel level.original for level in orderedLevels
level.locked = true if level.slug is 'kithgard-mastery' and @calculateExperienceScore() is 0 level.position ?= { x: 10, y: 10 }
level.locked = false if @levelStatusMap[level.slug] in ['started', 'complete'] level.locked = not me.ownsLevel(level.original) or previousIncompletePracticeLevel
level.locked = false if @editorMode level.locked = true if level.slug is 'kithgard-mastery' and @calculateExperienceScore() is 0
level.locked = false if @campaign?.get('name') in ['Auditions', 'Intro'] level.locked = false if @levelStatusMap[level.slug] in ['started', 'complete']
level.locked = false if me.isInGodMode() level.locked = false if @editorMode
#level.locked = false if level.slug is 'robot-ragnarok' level.locked = false if @campaign?.get('name') in ['Auditions', 'Intro']
level.disabled = true if level.adminOnly and @levelStatusMap[level.slug] not in ['started', 'complete'] level.locked = false if me.isInGodMode()
level.disabled = false if me.isInGodMode() #level.locked = false if level.slug is 'robot-ragnarok'
level.color = 'rgb(255, 80, 60)' level.disabled = true if level.adminOnly and @levelStatusMap[level.slug] not in ['started', 'complete']
if level.requiresSubscription level.disabled = false if me.isInGodMode()
level.color = 'rgb(80, 130, 200)' level.color = 'rgb(255, 80, 60)'
if level.adventurer level.color = 'rgb(80, 130, 200)' if level.requiresSubscription
level.color = 'rgb(200, 80, 200)' level.color = 'rgb(200, 80, 200)' if level.adventurer
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
level.purchasedHero = level.unlocksHero in (me.get('purchased')?.heroes or []) level.purchasedHero = level.unlocksHero in (me.get('purchased')?.heroes or [])
if window.serverConfig.picoCTF if window.serverConfig.picoCTF
if problem = _.find(@picoCTFProblems or [], pid: level.picoCTFProblem) if problem = _.find(@picoCTFProblems or [], pid: level.picoCTFProblem)
level.locked = false if problem.unlocked or level.slug is 'digital-graffiti' level.locked = false if problem.unlocked or level.slug is 'digital-graffiti'
#level.locked = false # Testing to see all levels #level.locked = false # Testing to see all levels
level.description = """ level.description = """
### #{problem.name} ### #{problem.name}
#{level.description or problem.description} #{level.description or problem.description}
#{problem.category} - #{problem.score} points #{problem.category} - #{problem.score} points
""" """
level.color = 'rgb(80, 130, 200)' if problem.solved level.color = 'rgb(80, 130, 200)' if problem.solved
level.hidden = level.locked if level.practice and not level.locked and @levelStatusMap[level.slug] isnt 'complete' and
if level.concepts?.length (not level.requiresSubscription or level.adventurer or not @requiresSubscription)
level.displayConcepts = level.concepts previousIncompletePracticeLevel = true
maxConcepts = 6
if level.displayConcepts.length > maxConcepts level.hidden = level.locked
level.displayConcepts = level.displayConcepts.slice -maxConcepts if level.concepts?.length
level level.displayConcepts = level.concepts
maxConcepts = 6
if level.displayConcepts.length > maxConcepts
level.displayConcepts = level.displayConcepts.slice -maxConcepts
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
findNextLevel = (nextLevels, practiceOnly) =>
for nextLevelOriginal in nextLevels
nextLevel = _.find orderedLevels, original: nextLevelOriginal
continue if not nextLevel or nextLevel.locked
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 nextLevel.slug is 'kithgard-mastery' and not @levelStatusMap[nextLevel.slug] and @calculateExperienceScore() >= 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.
dontPointTo = _.without dontPointTo, nextLevel.slug
storage.save "pointed-out-#{nextLevel.slug}", timesPointedOut + 1
# Should we point this level out?
if not nextLevel.disabled and @levelStatusMap[nextLevel.slug] isnt 'complete' and nextLevel.slug not in dontPointTo and
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])
)
nextLevel.next = true
return true
false
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. # 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) level.nextLevels = (reward.level for reward in level.rewards ? [] when reward.level)
unless foundNext break if foundNext = findNextLevel(level.nextLevels, true) # Check practice levels first
for nextLevelOriginal in level.nextLevels break if foundNext = findNextLevel(level.nextLevels, false)
nextLevel = _.find levels, original: nextLevelOriginal
# If it's a challenge level, we efficiently determine whether we actually do want to point it out. if not foundNext and orderedLevels[0] and not orderedLevels[0].locked and @levelStatusMap[orderedLevels[0].slug] isnt 'complete'
if nextLevel and nextLevel.slug is 'kithgard-mastery' and not nextLevel.locked and not @levelStatusMap[nextLevel.slug] and @calculateExperienceScore() >= 3 orderedLevels[0].next = true
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.
dontPointTo = _.without dontPointTo, nextLevel.slug
storage.save "pointed-out-#{nextLevel.slug}", timesPointedOut + 1
# 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 (
me.isPremium() or not nextLevel.requiresSubscription or
_.any(subscriptionPrompts, (prompt) => nextLevel.slug is prompt.slug and not @levelStatusMap[prompt.unless])
)
nextLevel.next = true
foundNext = true
break
if not foundNext and levels[0] and not levels[0].locked and @levelStatusMap[levels[0].slug] isnt 'complete'
levels[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)