mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2025-03-14 07:00:01 -04:00
Hints v1
Add per-level tips and tricks, available during gameplay to help unstick players. Closes #3736
This commit is contained in:
parent
c8e7b79e5d
commit
86fc4a3846
20 changed files with 347 additions and 104 deletions
|
@ -440,6 +440,8 @@
|
|||
tome_available_spells: "Available Spells"
|
||||
tome_your_skills: "Your Skills"
|
||||
tome_current_method: "Current Method"
|
||||
hints: "Hints"
|
||||
hints_title: "Hint {{number}}"
|
||||
code_saved: "Code Saved"
|
||||
skip_tutorial: "Skip (esc)"
|
||||
keyboard_shortcuts: "Key Shortcuts"
|
||||
|
|
|
@ -179,6 +179,18 @@ module.exports = class User extends CocoModel
|
|||
application.tracker.identify fourthLevelGroup: @fourthLevelGroup unless me.isAdmin()
|
||||
@fourthLevelGroup
|
||||
|
||||
getHintsGroup: ->
|
||||
# A/B testing two styles of hints
|
||||
return @hintsGroup if @hintsGroup
|
||||
group = me.get('testGroupNumber') % 3
|
||||
@hintsGroup = switch group
|
||||
when 0 then 'no-hints'
|
||||
when 1 then 'hints'
|
||||
when 2 then 'hintsB'
|
||||
@hintsGroup = 'hints' if me.isAdmin()
|
||||
application.tracker.identify hintsGroup: @hintsGroup unless me.isAdmin()
|
||||
@hintsGroup
|
||||
|
||||
getVideoTutorialStylesIndex: (numVideos=0)->
|
||||
# A/B Testing video tutorial styles
|
||||
# Not a constant number of videos available (e.g. could be 0, 1, 3, or 4 currently)
|
||||
|
|
41
app/styles/play/level/hints-view.sass
Normal file
41
app/styles/play/level/hints-view.sass
Normal file
|
@ -0,0 +1,41 @@
|
|||
@import "app/styles/mixins"
|
||||
|
||||
.hints-view
|
||||
position: relative
|
||||
|
||||
width: 500px // TODO: should be in sync with surface min-width
|
||||
padding: 10px 20px
|
||||
border-style: solid
|
||||
border-image: url(/images/level/popover_border_background.png) 16 12 fill round
|
||||
border-width: 16px 12px
|
||||
@include box-shadow(0 0 0 #000)
|
||||
|
||||
.close-hint-btn
|
||||
position: absolute
|
||||
right: 5px
|
||||
top: 5px
|
||||
|
||||
.glyphicon-remove
|
||||
position: relative
|
||||
top: 4px
|
||||
|
||||
h1
|
||||
margin-bottom: 30px
|
||||
|
||||
.btn-area
|
||||
margin-top: 20px
|
||||
|
||||
.hint-title
|
||||
font-size: 18px
|
||||
text-transform: uppercase
|
||||
|
||||
.hint-body
|
||||
height: 390px
|
||||
overflow-y: auto
|
||||
img
|
||||
width: 100%
|
||||
|
||||
.hint-pagination
|
||||
font-size: 18px
|
||||
margin-top: 0px
|
||||
text-transform: uppercase
|
|
@ -78,9 +78,6 @@
|
|||
@include flex-column()
|
||||
@include flex-align-content-start()
|
||||
|
||||
&.no-help
|
||||
margin-top: 3%
|
||||
|
||||
.property-entry-item-group
|
||||
display: inline-block
|
||||
min-height: 38px
|
|
@ -85,27 +85,18 @@
|
|||
.glyphicon-fullscreen
|
||||
display: none
|
||||
|
||||
.hints-button
|
||||
float: right
|
||||
border-style: solid
|
||||
border-image: url(/images/common/button-background-primary-active.png) 14 20 20 20 fill round
|
||||
border-width: 7px 10px 10px 10px
|
||||
color: white
|
||||
&:hover, &:active
|
||||
border-image: url(/images/common/button-background-primary-pressed.png) 14 20 20 20 fill round
|
||||
|
||||
.thang-avatar-wrapper
|
||||
border-width: 0
|
||||
|
||||
.method-name-area
|
||||
margin-left: 10px
|
||||
margin-top: 10px
|
||||
text-transform: uppercase
|
||||
display: inline-block
|
||||
font-family: $headings-font-family
|
||||
font-weight: bold
|
||||
|
||||
.method-label
|
||||
font-size: 12px
|
||||
color: rgb(243, 211, 59)
|
||||
margin-bottom: -5px
|
||||
|
||||
.method-signature
|
||||
color: white
|
||||
font-size: 18px
|
||||
padding: 0
|
||||
|
||||
.spell-list-entry-view:not(.spell-tab)
|
||||
cursor: pointer
|
||||
@include opacity(0.90)
|
||||
|
|
|
@ -264,6 +264,13 @@ $level-resize-transition-time: 0.5s
|
|||
width: 100%
|
||||
height: 90px
|
||||
text-align: center
|
||||
|
||||
.hints-view
|
||||
position: absolute
|
||||
top: 10px
|
||||
bottom: 10px
|
||||
right: 45%
|
||||
z-index: 1000000
|
||||
|
||||
html.fullscreen-editor
|
||||
#level-view
|
21
app/templates/play/level/hints-view.jade
Normal file
21
app/templates/play/level/hints-view.jade
Normal file
|
@ -0,0 +1,21 @@
|
|||
|
||||
button.close-hint-btn.btn.btn-illustrated.btn-danger
|
||||
span.glyphicon.glyphicon-remove
|
||||
|
||||
h1.text-center.hint-title
|
||||
span= view.state.get('hintsTitle')
|
||||
|
||||
.hint-body
|
||||
!= view.getProcessedHint()
|
||||
|
||||
.row.btn-area
|
||||
.col-md-4
|
||||
if view.state.get('hintIndex') > 0
|
||||
button.previous-btn.btn.btn-illustrated.pull-left(data-i18n="about.previous")
|
||||
.col-md-4
|
||||
h2.text-center.hint-pagination #{view.state.get('hintIndex')+1} / #{view.hintsState.get('total')}
|
||||
.col-md-4
|
||||
if view.state.get('hintIndex') < view.hintsState.get('total') - 1
|
||||
button.next-btn.btn.btn-illustrated.pull-right(data-i18n="about.next")
|
||||
|
||||
.clearfix
|
36
app/templates/play/level/tome/spell-palette-view.jade
Normal file
36
app/templates/play/level/tome/spell-palette-view.jade
Normal file
|
@ -0,0 +1,36 @@
|
|||
|
||||
div
|
||||
span.code-palette-background
|
||||
if view.entryGroupSlugs
|
||||
// Non-hero; group by entry groups, or maybe nothing.
|
||||
ul(class="nav nav-pills" + (tabbed ? ' multiple-tabs' : ''))
|
||||
each slug, group in view.entryGroupSlugs
|
||||
li(class=group == "this" || slug == "available-spells" ? "active" : "")
|
||||
a(data-toggle="pill", data-target='#palette-tab-' + slug)
|
||||
h4= view.entryGroupNames[group]
|
||||
.tab-content
|
||||
each slug, group in view.entryGroupSlugs
|
||||
div(id="palette-tab-" + slug, class="tab-pane nano" + (group == "this" || slug == view.defaultGroupSlug ? " active" : ""))
|
||||
div(class="properties properties-" + slug + " nano-content")
|
||||
|
||||
else if view.tabs
|
||||
// Hero; group by items, but also include tabs
|
||||
ul(class="nav nav-pills multiple-tabs")
|
||||
li.active
|
||||
a(data-toggle="pill", data-target="#palette-tab-this")
|
||||
h4= view.thisName
|
||||
each entries, tab in view.tabs
|
||||
li
|
||||
a(data-toggle="pill", data-target='#palette-tab-' + _.string.slugify(tab))
|
||||
h4= tab
|
||||
.tab-content
|
||||
div#palette-tab-this.tab-pane.active
|
||||
.properties.properties-this
|
||||
each entries, tab in tabs
|
||||
div(id="palette-tab-" + _.string.slugify(tab), class="tab-pane")
|
||||
div(class="properties properties-" + _.string.slugify(tab))
|
||||
|
||||
else
|
||||
// Hero; group by items, no tabs.
|
||||
br
|
||||
.properties.properties-this
|
|
@ -9,22 +9,22 @@ if includeSpellList
|
|||
|
||||
.thang-avatar-placeholder
|
||||
|
||||
.method-name-area
|
||||
.method-label(data-i18n="play_level.tome_current_method") Current Method
|
||||
.method-signature #{methodSignature}
|
||||
|
||||
.spell-tool-buttons
|
||||
.btn.btn-small.btn-illustrated.btn-warning.reload-code(data-i18n="[title]play_level.tome_reload_method", title="Reload original code for this method")
|
||||
.glyphicon.glyphicon-repeat
|
||||
span.spl(data-i18n="play_level.reload") Reload
|
||||
|
||||
|
||||
if me.level() >= 15
|
||||
.btn.btn-small.btn-illustrated.fullscreen-code(title=maximizeShortcutVerbose)
|
||||
.glyphicon.glyphicon-fullscreen
|
||||
.glyphicon.glyphicon-resize-small
|
||||
|
||||
|
||||
if codeLanguage === 'javascript' && me.level() >= 15
|
||||
.btn.btn-small.btn-illustrated.beautify-code(title=beautifyShortcutVerbose)
|
||||
.glyphicon.glyphicon-magnet
|
||||
|
||||
.clearfix
|
||||
if view.hintsState && view.hintsState.get('total') > 0
|
||||
.btn.btn-small.btn-illustrated.hints-button
|
||||
span(data-i18n="play_level.hints")
|
||||
|
||||
.clearfix
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
span.code-palette-background
|
||||
if entryGroupSlugs
|
||||
// Non-hero; group by entry groups, or maybe nothing.
|
||||
ul(class="nav nav-pills" + (tabbed ? ' multiple-tabs' : ''))
|
||||
each slug, group in entryGroupSlugs
|
||||
li(class=group == "this" || slug == "available-spells" ? "active" : "")
|
||||
a(data-toggle="pill", data-target='#palette-tab-' + slug)
|
||||
h4= entryGroupNames[group]
|
||||
.tab-content
|
||||
each slug, group in entryGroupSlugs
|
||||
div(id="palette-tab-" + slug, class="tab-pane nano" + (group == "this" || slug == defaultGroupSlug ? " active" : ""))
|
||||
div(class="properties properties-" + slug + " nano-content")
|
||||
|
||||
else if tabs
|
||||
// Hero; group by items, but also include tabs
|
||||
ul(class="nav nav-pills multiple-tabs")
|
||||
li.active
|
||||
a(data-toggle="pill", data-target="#palette-tab-this")
|
||||
h4= thisName
|
||||
each entries, tab in tabs
|
||||
li
|
||||
a(data-toggle="pill", data-target='#palette-tab-' + _.string.slugify(tab))
|
||||
h4= tab
|
||||
.tab-content
|
||||
div#palette-tab-this.tab-pane.active
|
||||
.properties.properties-this
|
||||
each entries, tab in tabs
|
||||
div(id="palette-tab-" + _.string.slugify(tab), class="tab-pane")
|
||||
div(class="properties properties-" + _.string.slugify(tab))
|
||||
|
||||
else
|
||||
// Hero; group by items, no tabs.
|
||||
if showsHelp
|
||||
button.btn.btn-sm.btn-info.banner#spell-palette-help-button(data-i18n="common.help")
|
||||
.properties.properties-this
|
||||
else
|
||||
.properties.properties-this.no-help
|
|
@ -49,6 +49,8 @@ if view.showAds()
|
|||
|
||||
button.btn.btn-lg.btn-warning.banner.header-font#stop-real-time-playback-button(title="Stop real-time playback", data-i18n="play_level.skip") Skip
|
||||
|
||||
.hints-view.hide
|
||||
|
||||
#level-footer-shadow
|
||||
#level-footer-background
|
||||
|
30
app/views/play/level/HintsState.coffee
Normal file
30
app/views/play/level/HintsState.coffee
Normal file
|
@ -0,0 +1,30 @@
|
|||
module.exports = class HintsState extends Backbone.Model
|
||||
|
||||
initialize: (attributes, options) ->
|
||||
{ @level, @session } = options
|
||||
@listenTo(@level, 'change:documentation', @update)
|
||||
@update()
|
||||
|
||||
getHint: (index) ->
|
||||
@get('hints')?[index]
|
||||
|
||||
update: ->
|
||||
hints = switch me.getHintsGroup()
|
||||
when 'hints' then @level.get('documentation')?.hints or []
|
||||
when 'hintsB' then @level.get('documentation')?.hintsB or []
|
||||
else []
|
||||
haveIntro = false
|
||||
haveOverview = false
|
||||
for article in @level.get('documentation')?.specificArticles ? []
|
||||
if not haveIntro and article.name is 'Intro'
|
||||
hints.unshift(article)
|
||||
haveIntro = true
|
||||
if not haveOverview and article.name is 'Overview'
|
||||
hints.push(article)
|
||||
haveOverview = true
|
||||
break if haveIntro and haveOverview
|
||||
total = _.size(hints)
|
||||
@set({
|
||||
hints: hints
|
||||
total
|
||||
})
|
94
app/views/play/level/HintsView.coffee
Normal file
94
app/views/play/level/HintsView.coffee
Normal file
|
@ -0,0 +1,94 @@
|
|||
CocoView = require 'views/core/CocoView'
|
||||
State = require 'models/State'
|
||||
utils = require 'core/utils'
|
||||
|
||||
module.exports = class HintsView extends CocoView
|
||||
template: require('templates/play/level/hints-view')
|
||||
className: 'hints-view'
|
||||
hintUsedThresholdSeconds: 10
|
||||
|
||||
events:
|
||||
'click .next-btn': 'onClickNextButton'
|
||||
'click .previous-btn': 'onClickPreviousButton'
|
||||
'click .close-hint-btn': 'hideView'
|
||||
|
||||
subscriptions:
|
||||
'level:show-victory': 'hideView'
|
||||
'tome:manual-cast': 'hideView'
|
||||
|
||||
initialize: (options) ->
|
||||
{@level, @session, @hintsState} = options
|
||||
@state = new State({
|
||||
hintIndex: 0
|
||||
hintsViewTime: {}
|
||||
hintsUsed: {}
|
||||
})
|
||||
@updateHint()
|
||||
|
||||
debouncedRender = _.debounce(@render)
|
||||
@listenTo(@state, 'change', debouncedRender)
|
||||
@listenTo(@hintsState, 'change', debouncedRender)
|
||||
@listenTo(@state, 'change:hintIndex', @updateHint)
|
||||
@listenTo(@hintsState, 'change:hidden', @visibilityChanged)
|
||||
|
||||
destroy: ->
|
||||
clearInterval(@timerIntervalID)
|
||||
super()
|
||||
|
||||
afterRender: ->
|
||||
@$el.toggleClass('hide', @hintsState.get('hidden'))
|
||||
super()
|
||||
|
||||
getProcessedHint: ->
|
||||
language = @session.get('codeLanguage')
|
||||
hint = @state.get('hint')
|
||||
return unless hint
|
||||
|
||||
# process
|
||||
translated = utils.i18n(hint, 'body')
|
||||
filtered = utils.filterMarkdownCodeLanguages(translated, language)
|
||||
markedUp = marked(filtered)
|
||||
|
||||
return markedUp
|
||||
|
||||
updateHint: ->
|
||||
index = @state.get('hintIndex')
|
||||
hintsTitle = $.i18n.t('play_level.hints_title').replace('{{number}}', index + 1)
|
||||
@state.set({ hintsTitle, hint: @hintsState.getHint(index) })
|
||||
|
||||
onClickNextButton: ->
|
||||
window.tracker?.trackEvent 'Hints Next Clicked', category: 'Students', levelSlug: @level.get('slug'), hintCount: @hintsState.get('hints')?.length ? 0, hintCurrent: @state.get('hintIndex'), ['Mixpanel']
|
||||
max = @hintsState.get('total') - 1
|
||||
@state.set('hintIndex', Math.min(@state.get('hintIndex') + 1, max))
|
||||
@playSound 'menu-button-click'
|
||||
@updateHintTimer()
|
||||
|
||||
onClickPreviousButton: ->
|
||||
window.tracker?.trackEvent 'Hints Previous Clicked', category: 'Students', levelSlug: @level.get('slug'), hintCount: @hintsState.get('hints')?.length ? 0, hintCurrent: @state.get('hintIndex'), ['Mixpanel']
|
||||
@state.set('hintIndex', Math.max(@state.get('hintIndex') - 1, 0))
|
||||
@playSound 'menu-button-click'
|
||||
@updateHintTimer()
|
||||
|
||||
hideView: -> @hintsState?.set('hidden', true)
|
||||
|
||||
visibilityChanged: (e) ->
|
||||
@updateHintTimer()
|
||||
|
||||
updateHintTimer: ->
|
||||
clearInterval(@timerIntervalID)
|
||||
unless @hintsState.get('hidden') or @state.get('hintsUsed')?[@state.get('hintIndex')]
|
||||
@timerIntervalID = setInterval(@incrementHintViewTime, 1000)
|
||||
|
||||
incrementHintViewTime: =>
|
||||
hintIndex = @state.get('hintIndex')
|
||||
hintsViewTime = @state.get('hintsViewTime')
|
||||
hintsViewTime[hintIndex] ?= 0
|
||||
hintsViewTime[hintIndex]++
|
||||
hintsUsed = @state.get('hintsUsed')
|
||||
if hintsViewTime[hintIndex] > @hintUsedThresholdSeconds and not hintsUsed[hintIndex]
|
||||
window.tracker?.trackEvent 'Hint Used', category: 'Students', levelSlug: @level.get('slug'), hintCount: @hintsState.get('hints')?.length ? 0, hintCurrent: hintIndex, ['Mixpanel']
|
||||
hintsUsed[hintIndex] = true
|
||||
@state.set('hintsUsed', hintsUsed)
|
||||
clearInterval(@timerIntervalID)
|
||||
@state.set('hintsViewTime', hintsViewTime)
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
RootView = require 'views/core/RootView'
|
||||
template = require 'templates/play/level'
|
||||
template = require 'templates/play/play-level-view'
|
||||
{me} = require 'core/auth'
|
||||
ThangType = require 'models/ThangType'
|
||||
utils = require 'core/utils'
|
||||
|
@ -41,6 +41,8 @@ PicoCTFVictoryModal = require './modal/PicoCTFVictoryModal'
|
|||
InfiniteLoopModal = require './modal/InfiniteLoopModal'
|
||||
LevelSetupManager = require 'lib/LevelSetupManager'
|
||||
ContactModal = require 'views/core/ContactModal'
|
||||
HintsView = require './HintsView'
|
||||
HintsState = require './HintsState'
|
||||
|
||||
PROFILE_ME = false
|
||||
|
||||
|
@ -259,7 +261,8 @@ module.exports = class PlayLevelView extends RootView
|
|||
@god.setGoalManager @goalManager
|
||||
|
||||
insertSubviews: ->
|
||||
@insertSubView @tome = new TomeView levelID: @levelID, session: @session, otherSession: @otherSession, thangs: @world.thangs, supermodel: @supermodel, level: @level, observing: @observing, courseID: @courseID, courseInstanceID: @courseInstanceID, god: @god
|
||||
@hintsState = new HintsState({ hidden: true }, { @session, @level })
|
||||
@insertSubView @tome = new TomeView { @levelID, @session, @otherSession, thangs: @world.thangs, @supermodel, @level, @observing, @courseID, @courseInstanceID, @god, @hintsState }
|
||||
@insertSubView new LevelPlaybackView session: @session, level: @level
|
||||
@insertSubView new GoalsView {}
|
||||
@insertSubView new LevelFlagsView levelID: @levelID, world: @world if @$el.hasClass 'flags'
|
||||
|
@ -270,6 +273,7 @@ module.exports = class PlayLevelView extends RootView
|
|||
@insertSubView new ProblemAlertView session: @session, level: @level, supermodel: @supermodel
|
||||
@insertSubView new DuelStatsView level: @level, session: @session, otherSession: @otherSession, supermodel: @supermodel, thangs: @world.thangs if @level.get('type') in ['hero-ladder', 'course-ladder']
|
||||
@insertSubView @controlBar = new ControlBarView {worldName: utils.i18n(@level.attributes, 'name'), session: @session, level: @level, supermodel: @supermodel, courseID: @courseID, courseInstanceID: @courseInstanceID}
|
||||
@insertSubView @hintsView = new HintsView({ @session, @level, @hintsState }), @$('.hints-view')
|
||||
#_.delay (=> Backbone.Mediator.publish('level:set-debug', debug: true)), 5000 if @isIPadApp() # if me.displayName() is 'Nick'
|
||||
|
||||
initVolume: ->
|
||||
|
|
|
@ -51,7 +51,12 @@ module.exports = class Spell
|
|||
if @canRead() # We can avoid creating these views if we'll never use them.
|
||||
@view = new SpellView {spell: @, level: options.level, session: @session, otherSession: @otherSession, worker: @worker, god: options.god, @supermodel}
|
||||
@view.render() # Get it ready and code loaded in advance
|
||||
@tabView = new SpellListTabEntryView spell: @, supermodel: @supermodel, codeLanguage: @language, level: options.level
|
||||
@tabView = new SpellListTabEntryView
|
||||
hintsState: options.hintsState
|
||||
spell: @
|
||||
supermodel: @supermodel
|
||||
codeLanguage: @language
|
||||
level: options.level
|
||||
@tabView.render()
|
||||
Backbone.Mediator.publish 'tome:spell-created', spell: @
|
||||
|
||||
|
|
|
@ -31,35 +31,10 @@ module.exports = class SpellListEntryView extends CocoView
|
|||
getRenderData: (context={}) ->
|
||||
context = super context
|
||||
context.spell = @spell
|
||||
context.methodSignature = @createMethodSignature()
|
||||
context.thangNames = (thangID for thangID, spellThang of @spell.thangs when spellThang.thang.exists).join(', ') # + ', Marcus, Robert, Phoebe, Will Smith, Zap Brannigan, You, Gandaaaaalf'
|
||||
context.showTopDivider = @showTopDivider
|
||||
context
|
||||
|
||||
createMethodSignature: ->
|
||||
return @spell.name if @options.level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev']
|
||||
parameters = (@spell.parameters or []).slice()
|
||||
if @spell.language in ['python', 'lua']
|
||||
parameters.unshift 'self'
|
||||
else if @spell.language is 'io'
|
||||
parameters.unshift '...'
|
||||
paramString = parameters.join ', '
|
||||
name = @spell.name
|
||||
switch @spell.language
|
||||
when 'python'
|
||||
"def #{name}(#{paramString}):"
|
||||
when 'lua'
|
||||
"function #{name}(#{paramString}) ... end"
|
||||
when 'coffeescript'
|
||||
if parameters.length
|
||||
"@#{name} = (#{paramString}) ->"
|
||||
else
|
||||
"@#{name} = ->"
|
||||
when 'javascript'
|
||||
"function #{name}(#{paramString}) { ... }"
|
||||
else
|
||||
"#{name}(#{paramString})"
|
||||
|
||||
getPrimarySpellThang: ->
|
||||
if @lastSelectedThang
|
||||
spellThang = _.find @spell.thangs, (spellThang) => spellThang.thang.id is @lastSelectedThang.id
|
||||
|
|
|
@ -23,9 +23,11 @@ module.exports = class SpellListTabEntryView extends SpellListEntryView
|
|||
'click .reload-code': 'onCodeReload'
|
||||
'click .beautify-code': 'onBeautifyClick'
|
||||
'click .fullscreen-code': 'onToggleMaximize'
|
||||
'click .hints-button': 'onClickHintsButton'
|
||||
|
||||
constructor: (options) ->
|
||||
super options
|
||||
@hintsState = options.hintsState
|
||||
super(options)
|
||||
|
||||
getRenderData: (context={}) ->
|
||||
context = super context
|
||||
|
@ -91,6 +93,11 @@ module.exports = class SpellListTabEntryView extends SpellListEntryView
|
|||
onDisableControls: (e) -> @toggleControls e, false
|
||||
onEnableControls: (e) -> @toggleControls e, true
|
||||
|
||||
onClickHintsButton: ->
|
||||
return unless @hintsState?
|
||||
@hintsState.set('hidden', not @hintsState.get('hidden'))
|
||||
window.tracker?.trackEvent 'Hints Clicked', category: 'Students', levelSlug: @options.level.get('slug'), hintCount: @hintsState.get('hints')?.length ? 0, ['Mixpanel']
|
||||
|
||||
onDropdownClick: (e) ->
|
||||
return unless @controlsEnabled
|
||||
Backbone.Mediator.publish 'tome:toggle-spell-list', {}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
CocoView = require 'views/core/CocoView'
|
||||
template = require 'templates/play/level/tome/spell_palette'
|
||||
{me} = require 'core/auth'
|
||||
filters = require 'lib/image_filter'
|
||||
SpellPaletteEntryView = require './SpellPaletteEntryView'
|
||||
|
@ -12,7 +11,7 @@ N_ROWS = 4
|
|||
|
||||
module.exports = class SpellPaletteView extends CocoView
|
||||
id: 'spell-palette-view'
|
||||
template: template
|
||||
template: require 'templates/play/level/tome/spell-palette-view'
|
||||
controlsEnabled: true
|
||||
|
||||
subscriptions:
|
||||
|
@ -24,13 +23,8 @@ module.exports = class SpellPaletteView extends CocoView
|
|||
events:
|
||||
'click #spell-palette-help-button': 'onClickHelp'
|
||||
|
||||
constructor: (options) ->
|
||||
super options
|
||||
@level = options.level
|
||||
@session = options.session
|
||||
@supermodel = options.supermodel
|
||||
@thang = options.thang
|
||||
@useHero = options.useHero
|
||||
initialize: (options) ->
|
||||
{@level, @session, @supermodel, @thang, @useHero} = options
|
||||
docs = @options.level.get('documentation') ? {}
|
||||
@showsHelp = docs.specificArticles?.length or docs.generalArticles?.length
|
||||
@createPalette()
|
||||
|
|
|
@ -122,6 +122,7 @@ module.exports = class TomeView extends CocoView
|
|||
unless method.cloneOf
|
||||
skipProtectAPI = @getQueryVariable 'skip_protect_api', (@options.levelID in ['gridmancer', 'minimax-tic-tac-toe'])
|
||||
spell = @spells[spellKey] = new Spell
|
||||
hintsState: @options.hintsState
|
||||
programmableMethod: method
|
||||
spellKey: spellKey
|
||||
pathComponents: pathPrefixComponents.concat(pathComponents)
|
||||
|
@ -219,7 +220,7 @@ module.exports = class TomeView extends CocoView
|
|||
updateSpellPalette: (thang, spell) ->
|
||||
return unless thang and @spellPaletteView?.thang isnt thang and thang.programmableProperties or thang.apiProperties
|
||||
useHero = /hero/.test(spell.getSource()) or not /(self[\.\:]|this\.|\@)/.test(spell.getSource())
|
||||
@spellPaletteView = @insertSubView new SpellPaletteView thang: thang, supermodel: @supermodel, programmable: spell?.canRead(), language: spell?.language ? @options.session.get('codeLanguage'), session: @options.session, level: @options.level, courseID: @options.courseID, courseInstanceID: @options.courseInstanceID, useHero: useHero
|
||||
@spellPaletteView = @insertSubView new SpellPaletteView { thang, @supermodel, programmable: spell?.canRead(), language: spell?.language ? @options.session.get('codeLanguage'), session: @options.session, level: @options.level, courseID: @options.courseID, courseInstanceID: @options.courseInstanceID, useHero }
|
||||
@spellPaletteView.toggleControls {}, spell.view.controlsEnabled if spell?.view # TODO: know when palette should have been disabled but didn't exist
|
||||
|
||||
spellFor: (thang, spellName) ->
|
||||
|
|
61
test/app/views/play/level/HintsView.spec.coffee
Normal file
61
test/app/views/play/level/HintsView.spec.coffee
Normal file
|
@ -0,0 +1,61 @@
|
|||
HintsView = require 'views/play/level/HintsView'
|
||||
factories = require 'test/app/factories'
|
||||
|
||||
hintWithCode = """
|
||||
Hint #2 rosebud
|
||||
|
||||
```python
|
||||
print('Hello World')
|
||||
```
|
||||
|
||||
```javascript
|
||||
console.log('Hello World')
|
||||
```
|
||||
"""
|
||||
|
||||
longHint = _.times(100, -> 'Beuller...').join('\n\n')
|
||||
|
||||
xdescribe 'HintsView', ->
|
||||
beforeEach ->
|
||||
level = factories.makeLevel({
|
||||
documentation: {
|
||||
hints: [
|
||||
{ body: 'Hint #1 xyzzy' }
|
||||
{ body: hintWithCode }
|
||||
{ body: longHint }
|
||||
]
|
||||
}
|
||||
})
|
||||
@session = factories.makeLevelSession({ playtime: 0 })
|
||||
@view = new HintsView({ level, @session })
|
||||
@view.render()
|
||||
jasmine.demoEl(@view.$el)
|
||||
|
||||
describe 'when the first hint is shown', ->
|
||||
|
||||
it 'does not show the previous button', ->
|
||||
expect(@view.$el.find('.previous-btn').length).toBe(0)
|
||||
|
||||
describe 'when the user has played for a while', ->
|
||||
|
||||
beforeEach ->
|
||||
@view.render()
|
||||
|
||||
it 'shows the first hint', ->
|
||||
expect(_.string.contains(@view.$el.text(), 'xyzzy')).toBe(true)
|
||||
|
||||
it 'shows the next hint button', ->
|
||||
expect(@view.$el.find('.next-btn').length).toBe(1)
|
||||
|
||||
it 'filters out all code blocks but those of the selected language', ->
|
||||
@session.set({
|
||||
codeLanguage: 'javascript'
|
||||
playtime: 9001
|
||||
})
|
||||
@view.state.set('hintIndex', 1)
|
||||
@view.render()
|
||||
|
||||
if _.string.contains(@view.$el.text(), 'print')
|
||||
fail('Python code snippet found, should be filtered out')
|
||||
if not _.string.contains(@view.$el.text(), 'console')
|
||||
fail('JavaScript code snippet not found')
|
Loading…
Reference in a new issue