mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-12-04 12:51:12 -05:00
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:
parent
8bef580909
commit
d20600b381
2 changed files with 112 additions and 62 deletions
|
@ -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
|
||||||
|
|
37
test/app/views/play/CampaignView.spec.coffee
Normal file
37
test/app/views/play/CampaignView.spec.coffee
Normal 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)
|
Loading…
Reference in a new issue