Replayable once-per-day increasing-difficulty level basics.

This commit is contained in:
Nick Winter 2015-01-05 10:44:17 -08:00
parent 61180c640d
commit 947811c01b
18 changed files with 80 additions and 13 deletions

View file

@ -311,6 +311,7 @@ self.setupDebugWorldToRunUntilFrame = function (args) {
self.debugWorld.levelSessionIDs = args.levelSessionIDs;
self.debugWorld.submissionCount = args.submissionCount;
self.debugWorld.flagHistory = args.flagHistory;
self.debugWorld.difficulty = args.difficulty;
if (args.level)
self.debugWorld.loadFromLevel(args.level, true);
self.debugWorld.debugging = true;
@ -371,6 +372,7 @@ self.runWorld = function runWorld(args) {
self.world.levelSessionIDs = args.levelSessionIDs;
self.world.submissionCount = args.submissionCount;
self.world.flagHistory = args.flagHistory || [];
self.world.difficulty = args.difficulty || 0;
if(args.level)
self.world.loadFromLevel(args.level, true);
self.world.preloading = args.preload;

View file

@ -39,7 +39,7 @@ module.exports = class Angel extends CocoClass
# say: debugging stuff, usually off; log: important performance indicators, keep on
say: (args...) -> #@log args...
log: ->
log: ->
# console.info.apply is undefined in IE9, CofeeScript splats invocation won't work.
# http://stackoverflow.com/questions/5472938/does-ie9-support-console-log-and-is-it-a-real-function
message = "|#{@shared.godNick}'s #{@nick}|"
@ -246,6 +246,7 @@ module.exports = class Angel extends CocoClass
work.testWorld.levelSessionIDs = work.levelSessionIDs
work.testWorld.submissionCount = work.submissionCount
work.testWorld.flagHistory = work.flagHistory ? []
work.testWorld.difficulty = work.difficulty
testWorld.loadFromLevel work.level
work.testWorld.preloading = work.preload
work.testWorld.headless = work.headless

View file

@ -63,6 +63,7 @@ module.exports = class God extends CocoClass
onTomeCast: (e) ->
@lastSubmissionCount = e.submissionCount
@lastFlagHistory = (flag for flag in e.flagHistory when flag.source isnt 'code')
@lastDifficulty = e.difficulty
@createWorld e.spells, e.preload, e.realTime
createWorld: (spells, preload, realTime) ->
@ -92,6 +93,7 @@ module.exports = class God extends CocoClass
levelSessionIDs: @levelSessionIDs
submissionCount: @lastSubmissionCount
flagHistory: @lastFlagHistory
difficulty: @lastDifficulty
goals: @angelsShare.goalManager?.getGoals()
headless: @angelsShare.headless
preload: preload
@ -123,6 +125,7 @@ module.exports = class God extends CocoClass
levelSessionIDs: @levelSessionIDs
submissionCount: @lastSubmissionCount
flagHistory: @lastFlagHistory
difficulty: @lastDifficulty
goals: @goalManager?.getGoals()
frame: args.frame
currentThangID: args.thangID

View file

@ -374,6 +374,7 @@ module.exports = class LevelLoader extends CocoClass
@world.levelSessionIDs = if @opponentSessionID then [@sessionID, @opponentSessionID] else [@sessionID]
@world.submissionCount = @session?.get('state')?.submissionCount ? 0
@world.flagHistory = @session?.get('state')?.flagHistory ? []
@world.difficulty = @session?.get('state')?.difficulty ? 0
serializedLevel = @level.serialize(@supermodel, @session, @opponentSession)
@world.loadFromLevel serializedLevel, false
console.log 'World has been initialized from level loader.'

View file

@ -360,7 +360,7 @@ module.exports = class World
#console.log "... world serializing frames from", startFrame, "to", endFrame, "of", @totalFrames
[transferableObjects, nontransferableObjects] = [0, 0]
delete flag.processed for flag in @flagHistory
o = {totalFrames: @totalFrames, maxTotalFrames: @maxTotalFrames, frameRate: @frameRate, dt: @dt, victory: @victory, userCodeMap: {}, trackedProperties: {}, flagHistory: @flagHistory}
o = {totalFrames: @totalFrames, maxTotalFrames: @maxTotalFrames, frameRate: @frameRate, dt: @dt, victory: @victory, userCodeMap: {}, trackedProperties: {}, flagHistory: @flagHistory, difficulty: @difficulty}
o.trackedProperties[prop] = @[prop] for prop in @trackedProperties or []
for thangID, methods of @userCodeMap
@ -467,7 +467,7 @@ module.exports = class World
w.userCodeMap[thangID][methodName][aetherStateKey] = serializedAether[aetherStateKey]
else
w = new World o.userCodeMap, classMap
[w.totalFrames, w.maxTotalFrames, w.frameRate, w.dt, w.scriptNotes, w.victory, w.flagHistory] = [o.totalFrames, o.maxTotalFrames, o.frameRate, o.dt, o.scriptNotes ? [], o.victory, o.flagHistory]
[w.totalFrames, w.maxTotalFrames, w.frameRate, w.dt, w.scriptNotes, w.victory, w.flagHistory, w.difficulty] = [o.totalFrames, o.maxTotalFrames, o.frameRate, o.dt, o.scriptNotes ? [], o.victory, o.flagHistory, o.difficulty]
w[prop] = val for prop, val of o.trackedProperties
perf.t1 = now()

View file

@ -53,3 +53,16 @@ module.exports = class LevelSession extends CocoModel
save: (attrs, options) ->
return if @shouldAvoidCorruptData attrs
super attrs, options
increaseDifficulty: ->
state = @get('state') ? {}
state.difficulty = (state.difficulty ? 0) + 1
delete state.lastUnsuccessfulSubmissionTime
@set 'state', state
timeUntilResubmit: ->
state = @get('state') ? {}
return 0 unless last = state.lastUnsuccessfulSubmissionTime
last = new Date(last) if _.isString last
# Wait at least this long before allowing submit button active again.
(last - new Date()) + 22 * 60 * 60 * 1000

View file

@ -299,6 +299,7 @@ _.extend LevelSchema.properties,
style: c.shortString title: 'Style', description: 'Like: original, eccentric, scripted, edited, etc.'
free: {type: 'boolean', title: 'Free', description: 'Whether this video is freely available to all players without a subscription.'}
url: c.url {title: 'URL', description: 'Link to the video on Vimeo.'}
replayable: {type: 'boolean', title: 'Replayable', description: 'Whether this (hero) level infinitely scales up its difficulty and can be beaten over and over for greater rewards.'}
# Admin flags
adventurer: { type: 'boolean' }

View file

@ -114,6 +114,12 @@ _.extend LevelSessionSchema.properties,
description: 'How many times the session has been submitted for real-time playback (can affect the random seed).'
type: 'integer'
minimum: 0
difficulty:
description: 'The highest difficulty level beaten, for use in increasing-difficulty replayable levels.'
type: 'integer'
minimum: 0
lastUnsuccessfulSubmissionTime: c.date
description: 'The last time that real-time submission was started without resulting in a win.'
flagHistory:
description: 'The history of flag events during the last real-time playback submission.'
type: 'array'

View file

@ -7,16 +7,20 @@ module.exports =
preload: {type: 'boolean'}
realTime: {type: 'boolean'}
'tome:cast-spells': c.object {title: 'Cast Spells', description: 'Published when spells are cast', required: ['spells', 'preload', 'realTime', 'submissionCount', 'flagHistory']},
'tome:cast-spells': c.object {title: 'Cast Spells', description: 'Published when spells are cast', required: ['spells', 'preload', 'realTime', 'submissionCount', 'flagHistory', 'difficulty']},
spells: [type: 'object']
preload: [type: 'boolean']
realTime: [type: 'boolean']
submissionCount: [type: 'integer']
flagHistory: [type: 'array']
difficulty: [type: 'integer']
'tome:manual-cast': c.object {title: 'Manually Cast Spells', description: 'Published when you wish to manually recast all spells', required: []},
realTime: {type: 'boolean'}
'tome:manual-cast-denied': c.object {title: 'Manual Cast Denied', description: 'Published when player attempts to submit for real-time playback, but must wait after a replayable level failure.', required: ['timeUntilResubmit']},
timeUntilResubmit: {type: 'number'}
'tome:spell-created': c.object {title: 'Spell Created', description: 'Published after a new spell has been created', required: ['spell']},
spell: {type: 'object'}

View file

@ -1,7 +1,9 @@
button.btn.btn-lg.btn-illustrated.cast-button(title=castVerbose)
span(data-i18n="play_level.tome_run_button_ran") Ran
button.btn.btn-lg.btn-illustrated.submit-button(title=castRealTimeVerbose, data-i18n="play_level.tome_submit_button") Submit
button.btn.btn-lg.btn-illustrated.submit-button(title=castRealTimeVerbose)
span(data-i18n="play_level.tome_submit_button") Submit
span.spl.secret.submit-again-time
button.btn.btn-lg.btn-illustrated.done-button.secret
span(data-i18n="play_level.done") Done

View file

@ -63,7 +63,7 @@ module.exports = class AchievementEditView extends RootView
@$el.find('#achievement-view').empty()
for key, value of @treema.data
@achievement.set key, value
earned = earnedPoints: @achievement.get 'worth'
earned = get: (key) => {earnedPoints: @achievement.get('worth'), previouslyAchievedAmount: 0}[key]
popup = new AchievementPopup achievement: @achievement, earnedAchievement: earned, popup: false, container: $('#achievement-view')
openSaveModal: ->

View file

@ -15,7 +15,7 @@ module.exports = class SettingsTabView extends CocoView
editableSettings: [
'name', 'description', 'documentation', 'nextLevel', 'background', 'victory', 'i18n', 'icon', 'goals',
'type', 'terrain', 'showsGuide', 'banner', 'employerDescription', 'loadingTip', 'requiresSubscription',
'tasks', 'helpVideos'
'tasks', 'helpVideos', 'replayable'
]
subscriptions:

View file

@ -520,6 +520,7 @@ module.exports = class PlayLevelView extends RootView
return if @destroyed
# TODO: Show a victory dialog specific to hero-ladder level
if @goalManager.checkOverallStatus() is 'success' and not @options.realTimeMultiplayerSessionID?
@session.increaseDifficulty() if @level.get 'replayable'
Backbone.Mediator.publish 'level:show-victory', showModal: true
destroy: ->

View file

@ -14,6 +14,7 @@ module.exports = class CastButtonView extends CocoView
subscriptions:
'tome:spell-changed': 'onSpellChanged'
'tome:cast-spells': 'onCastSpells'
'tome:manual-cast-denied': 'onManualCastDenied'
'god:new-world-created': 'onNewWorld'
'real-time-multiplayer:created-game': 'onJoinedRealTimeMultiplayerGame'
'real-time-multiplayer:joined-game': 'onJoinedRealTimeMultiplayerGame'
@ -25,6 +26,11 @@ module.exports = class CastButtonView extends CocoView
super options
@spells = options.spells
@castShortcut = '⇧↵'
@updateReplayabilityInterval = setInterval @updateReplayability, 1000
destroy: ->
clearInterval @updateReplayabilityInterval
super()
getRenderData: (context={}) ->
context = super context
@ -49,6 +55,7 @@ module.exports = class CastButtonView extends CocoView
@$el.find('.done-button').show()
if @options.level.get('slug') is 'thornbush-farm'# and not @options.session.get('state')?.complete
@$el.find('.submit-button').hide() # Hide submit until first win so that script can explain it.
@updateReplayability()
attachTo: (spellView) ->
@$el.detach().prependTo(spellView.toolbarView.$el).show()
@ -59,8 +66,11 @@ module.exports = class CastButtonView extends CocoView
onCastRealTimeButtonClick: (e) ->
if @inRealTimeMultiplayerSession
Backbone.Mediator.publish 'real-time-multiplayer:manual-cast', {}
else if @options.level.get('replayable') and (timeUntilResubmit = @options.session.timeUntilResubmit()) > 0
Backbone.Mediator.publish 'tome:manual-cast-denied', timeUntilResubmit: timeUntilResubmit
else
Backbone.Mediator.publish 'tome:manual-cast', {realTime: true}
@updateReplayability()
onDoneButtonClick: (e) ->
Backbone.Mediator.publish 'level:show-victory', showModal: true
@ -72,14 +82,19 @@ module.exports = class CastButtonView extends CocoView
return if e.preload
@casting = true
if @hasStartedCastingOnce # Don't play this sound the first time
Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'cast', volume: 0.5
@playSound 'cast', 0.5
@hasStartedCastingOnce = true
@updateCastButton()
onManualCastDenied: (e) ->
wait = moment().add(e.timeUntilResubmit, 'ms').fromNow()
#@playSound 'manual-cast-denied', 1.0 # find some sound for this?
noty text: "You can try again #{wait}.", layout: 'center', type: 'warning', killer: false, timeout: 6000
onNewWorld: (e) ->
@casting = false
if @hasCastOnce # Don't play this sound the first time
Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'cast-end', volume: 0.5
@playSound 'cast-end', 0.5
@hasCastOnce = true
@updateCastButton()
@ -120,6 +135,17 @@ module.exports = class CastButtonView extends CocoView
@castButton.text castText
#@castButton.prop 'disabled', not castable
updateReplayability: =>
return if @destroyed
return unless @options.level.get 'replayable'
timeUntilResubmit = @options.session.timeUntilResubmit()
disabled = timeUntilResubmit > 0
submitButton = @$el.find('.submit-button').toggleClass('disabled', disabled)
submitAgainLabel = submitButton.find('.submit-again-time').toggleClass('secret', not disabled)
if disabled
waitTime = moment().add(timeUntilResubmit, 'ms').fromNow()
submitAgainLabel.text waitTime
setAutocastDelay: (delay) ->
#console.log 'Set autocast delay to', delay
return unless delay

View file

@ -122,7 +122,11 @@ module.exports = class SpellView extends CocoView
addCommand
name: 'run-code-real-time'
bindKey: {win: 'Ctrl-Shift-Enter', mac: 'Command-Shift-Enter|Ctrl-Shift-Enter'}
exec: -> Backbone.Mediator.publish 'tome:manual-cast', {realTime: true}
exec: =>
if @options.level.get('replayable') and (timeUntilResubmit = @session.timeUntilResubmit()) > 0
Backbone.Mediator.publish 'tome:manual-cast-denied', timeUntilResubmit: timeUntilResubmit
else
Backbone.Mediator.publish 'tome:manual-cast', {realTime: true}
addCommand
name: 'no-op'
bindKey: {win: 'Ctrl-S', mac: 'Command-S|Ctrl-S'}
@ -616,7 +620,7 @@ module.exports = class SpellView extends CocoView
onSignificantChange.push _.debounce @checkSuspectCode, 750 if @options.level.get 'suspectCode'
@onCodeChangeMetaHandler = =>
return if @eventsSuppressed
Backbone.Mediator.publish 'audio-player:play-sound', trigger: 'code-change', volume: 0.5
#@playSound 'code-change', volume: 0.5 # Currently not using this sound.
if @spellThang
@spell.hasChangedSignificantly @getSource(), @spellThang.aether.raw, (hasChanged) =>
if not @spellThang or hasChanged

View file

@ -163,8 +163,9 @@ module.exports = class TomeView extends CocoView
if realTime
sessionState.submissionCount = (sessionState.submissionCount ? 0) + 1
sessionState.flagHistory = _.filter sessionState.flagHistory ? [], (event) => event.team isnt (@options.session.get('team') ? 'humans')
sessionState.lastUnsuccessfulSubmissionTime = new Date() if @options.level.get 'replayable'
@options.session.set 'state', sessionState
Backbone.Mediator.publish 'tome:cast-spells', spells: @spells, preload: preload, realTime: realTime, submissionCount: sessionState.submissionCount ? 0, flagHistory: sessionState.flagHistory ? []
Backbone.Mediator.publish 'tome:cast-spells', spells: @spells, preload: preload, realTime: realTime, submissionCount: sessionState.submissionCount ? 0, flagHistory: sessionState.flagHistory ? [], difficulty: sessionState.difficulty ? 0
onToggleSpellList: (e) ->
@spellList.rerenderEntries()

View file

@ -80,6 +80,7 @@ work = () ->
self.world.levelSessionIDs = args.levelSessionIDs
self.world.submissionCount = args.submissionCount
self.world.flagHistory = args.flagHistory
self.world.difficulty = args.difficulty
self.world.loadFromLevel args.level, true if args.level
self.world.headless = args.headless
self.goalManager = new GoalManager(self.world)

View file

@ -55,6 +55,7 @@ LevelHandler = class LevelHandler extends Handler
'tasks'
'helpVideos'
'campaign'
'replayable'
]
postEditableProperties: ['name']
@ -383,7 +384,7 @@ LevelHandler = class LevelHandler extends Handler
# Build list of level average playtimes
playtimes = []
for item in data
playtimes.push
playtimes.push
level: item._id.level
created: item._id.created
average: item.average