This commit is contained in:
Popey Gilbert 2015-01-31 10:10:24 -08:00
commit ec5d54cc97
45 changed files with 666 additions and 121 deletions

Binary file not shown.

Before

(image error) Size: 59 KiB

View file

@ -88,6 +88,8 @@ module.exports = class LevelLoader extends CocoClass
@listenToOnce @opponentSession, 'sync', @loadDependenciesForSession
loadDependenciesForSession: (session) ->
if me.id isnt session.get 'creator'
session.patch = session.save = -> console.error "Not saving session, since we didn't create it."
if session is @session
codeLanguage = session.get('codeLanguage') or me.get('aceConfig')?.language or 'python'
modulePath = "vendor/aether-#{codeLanguage}"
@ -98,6 +100,7 @@ module.exports = class LevelLoader extends CocoClass
if e.id is modulePath
@languageModuleResource.markLoaded()
@stopListening application.moduleLoader
@addSessionBrowserInfo session
# hero-ladder games require the correct session team in level:loaded
team = @team ? @session.get('team')
@ -134,6 +137,17 @@ module.exports = class LevelLoader extends CocoClass
if _.size(@sessionDependenciesRegistered) is 2 and @checkAllWorldNecessitiesRegisteredAndLoaded()
@onWorldNecessitiesLoaded()
addSessionBrowserInfo: (session) ->
return unless me.id is session.get 'creator'
return unless $.browser?
browser = {}
browser['desktop'] = $.browser.desktop if $.browser.desktop
browser['name'] = $.browser.name if $.browser.name
browser['platform'] = $.browser.platform if $.browser.platform
browser['version'] = $.browser.version if $.browser.version
session.set 'browser', browser
session.patch()
consolidateFlagHistory: ->
state = @session.get('state') ? {}
myFlagHistory = _.filter state.flagHistory ? [], team: @session.get('team')

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, difficulty: @difficulty}
o = {totalFrames: @totalFrames, maxTotalFrames: @maxTotalFrames, frameRate: @frameRate, dt: @dt, victory: @victory, userCodeMap: {}, trackedProperties: {}, flagHistory: @flagHistory, difficulty: @difficulty, scores: @getScores()}
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, w.difficulty] = [o.totalFrames, o.maxTotalFrames, o.frameRate, o.dt, o.scriptNotes ? [], o.victory, o.flagHistory, o.difficulty]
[w.totalFrames, w.maxTotalFrames, w.frameRate, w.dt, w.scriptNotes, w.victory, w.flagHistory, w.difficulty, w.scores] = [o.totalFrames, o.maxTotalFrames, o.frameRate, o.dt, o.scriptNotes ? [], o.victory, o.flagHistory, o.difficulty, o.scores]
w[prop] = val for prop, val of o.trackedProperties
perf.t1 = now()
@ -603,3 +603,10 @@ module.exports = class World
teamForPlayer: (n) ->
playableTeams = @playableTeams ? ['humans']
playableTeams[n % playableTeams.length]
getScores: ->
time: @age
'damage-taken': @getSystem('Combat')?.damageTakenForTeam 'humans'
'damage-dealt': @getSystem('Combat')?.damageDealtForTeam 'humans'
'gold-collected': @getSystem('Inventory')?.teamGold.humans?.earned
'difficulty': @difficulty

View file

@ -78,7 +78,7 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
awaiting_levels_adventurer_prefix: "Wir veröffentlichen fünf Levels pro Woche."
awaiting_levels_adventurer: "Registriere dich als ein Abenteurer"
awaiting_levels_adventurer_suffix: "sei der Erste, der neue Levels spielt."
# adjust_volume: "Adjust volume"
adjust_volume: "Lautstärke anpassen"
choose_your_level: "Wähle dein Level" # The rest of this section is the old play view at /play-old and isn't very important.
adventurer_prefix: "Du kannst zu jedem Level springen oder diskutiere die Level "
adventurer_forum: "im Abenteurerforum"
@ -157,10 +157,10 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
date: "Datum"
body: "Inhalt"
version: "Version"
# pending: "Pending"
# accepted: "Accepted"
# rejected: "Rejected"
# withdrawn: "Withdrawn"
pending: "ausstehend"
accepted: "akzeptiert"
rejected: "abgelehnt"
withdrawn: "zurückgezogen"
submitter: "Übermittler"
submitted: "Übermittelt"
commit_msg: "Übertrage Nachricht"
@ -168,10 +168,10 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
version_history: "Versionshistorie"
version_history_for: "Versionsgeschichte für: "
select_changes: "Wähle zwei Änderungen unten um den Unterschied sehen zu können."
# undo_prefix: "Undo"
# undo_shortcut: "(Ctrl+Z)"
undo_prefix: "Rückgängig"
undo_shortcut: "(Strg+Z)"
# redo_prefix: "Redo"
# redo_shortcut: "(Ctrl+Shift+Z)"
redo_shortcut: "(Strg+Umschalt+Z)"
play_preview: "Spiele eine Vorschau des momentanen Levels"
result: "Ergebnis"
results: "Ergebnisse"
@ -370,9 +370,9 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
unsubscribe: "Abmelden"
# confirm_unsubscribe: "Confirm Unsubscribe"
# never_mind: "Never Mind, I Still Love You"
# thank_you_months_prefix: "Thank you for supporting us these last"
# thank_you_months_suffix: "months."
# thank_you: "Thank you for supporting CodeCombat."
thank_you_months_prefix: "Danke für deine Unterstützung in den letzten"
thank_you_months_suffix: "Monaten."
thank_you: "Danke das du CodeCombat unterstützt."
# sorry_to_see_you_go: "Sorry to see you go! Please let us know what we could have done better."
# unsubscribe_feedback_placeholder: "O, what have we done?"
levels: "25 weitere level! Und 5 neue jede Woche!"
@ -627,7 +627,7 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
# desert: "Desert"
grassy: "Grasig"
small: "Klein"
# large: "Large"
large: "Groß"
fork_title: "Forke neue Version"
fork_creating: "Erzeuge Fork..."
generate_terrain: "Generiere Terrain"
@ -648,7 +648,7 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription:
level_tab_thangs_all: "Alle"
level_tab_thangs_conditions: "Startbedingungen"
level_tab_thangs_add: "Thangs hinzufügen"
# add_components: "Add Components"
add_components: "Kommentar hinzufügen"
# component_configs: "Component Configurations"
# config_thang: "Double click to configure a thang"
delete: "Löschen"

View file

@ -340,6 +340,19 @@
multiplayer_caption: "Play with friends!"
auth_caption: "Save your progress."
leaderboard:
leaderboard: "Leaderboard"
view_other_solutions: "View Other Solutions"
top_solutions: "Top Solutions"
day: "Today"
week: "This Week"
all: "All-Time"
time: "Time"
damage_taken: "Damage Taken"
damage_dealt: "Damage Dealt"
difficulty: "Difficulty"
gold_collected: "Gold Collected"
inventory:
choose_inventory: "Equip Items"
equipped_item: "Equipped"
@ -490,6 +503,46 @@
matt_title: "Programmer"
matt_blurb: "Bicyclist"
teachers:
title: "CodeCombat for Teachers"
preparation_title: "Preparation"
preparation_1: "CodeCombat is free to play for the core level progression and does not require students to sign up. We encourage teachers to"
preparation_play_campaign: "play through the campaign"
preparation_2: "to try it out, but the only thing you absolutely need to do to be ready is ensure students have access to a computer."
preparation_3: "It is not necessary for teachers to be comfortable with computer science concepts for students to have fun learning with CodeCombat."
violent_title: "Is it violent?"
violent_1: "We get this from teachers a lot due to our name. Although CodeCombat does contain cartoon violence, there is nothing graphic in either the visuals or language."
violent_2: "If you are comfortable having your students play Angry Birds, you will be comfortable with CodeCombat."
for_girls_title: "Is it for girls?"
for_girls_1: "There are three game modes in CodeCombat: building, puzzles, and combat. We have intentionally designed each to appeal to both boys and girls and think that the building and puzzle levels especially differentiate the game from violent triple A titles that repel female players."
what_cover_title: "What do we cover?"
what_cover_1: "There are 20 levels in the Hour of Code tutorial that teach and reinforce 6 specific computer science concepts:"
what_cover_notation_1: "Formal notation"
what_cover_notation_2: "- builds an understanding of the importance of syntax in programming."
what_cover_methods_1: "Calling methods"
what_cover_methods_2: "- familiarizes students with the syntax of object-oriented method calls."
what_cover_parameters_1: "Parameters"
what_cover_parameters_2: "- trains how to pass parameters to functions."
what_cover_strings_1: "Strings"
what_cover_strings_2: "- teaches students about string notation and passing strings as parameters."
what_cover_loops_1: "Loops"
what_cover_loops_2: "- develops the abstraction of designing short programs with loops."
what_cover_variables_1: "Variables"
what_cover_variables_2: "- adds the skill of referencing values that change over time."
what_cover_2: "Students may continue past level 20, depending on their speed and interest, to learn two additional concepts in later levels:"
what_cover_logic_1: "Conditional logic"
what_cover_logic_2: "- when and how to use if/else to control in-game outcomes."
what_cover_input_1: "Handling player input"
what_cover_input_2: "- responding to input events to create a user interface."
sys_requirements_title: "System Requirements"
sys_requirements_1: "Because CodeCombat is a game, it is more intensive for computers to run smoothly than video or written tutorials. We have optimized it to run quickly on all modern browsers and on older machines so that everyone can play. That said, here are our suggestions for getting the most out of your Hour of Code experience:"
sys_requirements_2: "Use newer versions of Chrome or Firefox."
sys_requirements_3: "Although CodeCombat will work on browsers as old as IE9, the performance is not as good. Chrome is best."
sys_requirements_4: "Use newer computers."
sys_requirements_5: "Older computers, Chromebooks, and netbooks tend to have very few system resources, which makes for a less enjoyable experience. At least 2GB of RAM is required."
sys_requirements_6: "Allow players to wear headphones/earbuds to hear the audio."
sys_requirements_7: "We help players learn through voiceover and sound effects, which will make classrooms noisy and distracting."
versions:
save_version_title: "Save New Version"
new_major_version: "New Major Version"

View file

@ -68,3 +68,22 @@ module.exports = class LevelSession extends CocoModel
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
recordScores: (scores, level) ->
state = @get 'state'
oldTopScores = state.topScores ? []
newTopScores = []
now = new Date()
for scoreType in level.get('scoreTypes') ? []
oldTopScore = _.find oldTopScores, type: scoreType
newScore = scores[scoreType]
unless newScore?
newTopScores.push oldTopScore
continue
newScore *= -1 if scoreType in ['time', 'damage-taken'] # Make it so that higher is better
if not oldTopScore? or newScore > oldTopScore.score
newTopScores.push type: scoreType, date: now, score: newScore
else
newTopScores.push oldTopScore
state.topScores = newTopScores
@set 'state', state

View file

@ -133,6 +133,21 @@ module.exports = class User extends CocoModel
application.tracker.identify foreshadowsLevels: @foreshadowsLevels unless me.isAdmin()
@foreshadowsLevels
getLeaderboardsGroup: ->
return @leaderboardsGroup if @leaderboardsGroup?
group = me.get('testGroupNumber') % 64
if group < 16
@leaderboardsGroup = 'always'
else if group < 32
@leaderboardsGroup = 'early'
else if group < 48
@leaderboardsGroup = 'late'
else
@leaderboardsGroup = 'never'
@leaderboardsGroup = 'always' if me.isAdmin()
application.tracker.identify leaderboardsGroup: @leaderboardsGroup unless me.isAdmin()
@leaderboardsGroup
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)

View file

@ -19,6 +19,7 @@ defaultTasks = [
'Publish.'
'Choose level options like required/restricted gear.'
'Create achievements, including unlocking next level.'
'Choose leaderboard score types.'
'Playtest with a slow/tough hero.'
'Playtest with a fast/weak hero.'
@ -341,6 +342,9 @@ _.extend LevelSchema.properties,
type: 'string', links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], format: 'latest-version-original-reference'
}}
campaign: c.shortString title: 'Campaign', description: 'Which campaign this level is part of (like "desert").', format: 'hidden' # Automatically set by campaign editor.
scoreTypes: c.array {title: 'Score Types', description: 'What metric to show leaderboards for.', uniqueItems: true},
c.shortString(title: 'Score Type', 'enum': ['time', 'damage-taken', 'damage-dealt', 'gold-collected', 'difficulty']) # TODO: good version of LoC; total gear value.
c.extendBasicProperties LevelSchema, 'level'
c.extendSearchableProperties LevelSchema

View file

@ -29,6 +29,8 @@ LevelSessionSchema = c.object
_.extend LevelSessionSchema.properties,
# denormalization
browser:
type: 'object'
creatorName:
type: 'string'
levelName:
@ -133,6 +135,12 @@ _.extend LevelSessionSchema.properties,
x: {type: 'number'}
y: {type: 'number'}
source: {type: 'string', enum: ['click']} # Do not store 'code' flag events in the session.
topScores: c.array {},
c.object {},
type: c.shortString('enum': ['time', 'damage-taken', 'damage-dealt', 'gold-collected', 'difficulty'])
date: c.date
description: 'When the submission achieving this score happened.'
score: {type: 'number'} # Store 'time' and 'damage-taken' as negative numbers so the index works.
code:
type: 'object'

View file

@ -4,6 +4,3 @@
float: right
width: 300px
margin-left: 20px
.cc-license-link
margin-left: 10px

View file

@ -320,6 +320,11 @@
margin: 0
float: left
.leaderboard-button
height: 60px
line-height: 30px
margin: 0 10px
float: left
.next-level-buttons
float: right

View file

@ -0,0 +1,94 @@
@import "app/styles/bootstrap/variables"
@import "app/styles/mixins"
#leaderboard-modal
//- Clear modal defaults
.modal-dialog
width: 820px
height: 570px
padding: 0
background: none
position: relative
top: 40px
//- Background
#leaderboard-background
position: absolute
top: -146px
left: -3px
//- Close modal button
#close-modal
position: absolute
left: 769px
top: -5px
width: 60px
height: 60px
color: white
text-align: center
font-size: 30px
padding-top: 17px
cursor: pointer
z-index: 2
@include rotate(-3deg)
&:hover
color: yellow
//- Nav bar
#leaderboard-nav
position: absolute
top: 53px
left: 42px
width: 178px
li
background: url(/images/pages/play/modal/menu-tab.png)
padding: 5px
margin: -5px 0
height: 80px
padding: 0
&.active
background: url(/images/pages/play/modal/menu-tab-selected.png)
width: 197px
a
font-size: 18px
line-height: 25px
background: none
color: rgb(195,153,124)
font-weight: bold
padding: 14px 20px
font-family: $headings-font-family
text-transform: uppercase
.timespan
margin-left: 20px
opacity: 0.75
//- Tab panels
.leaderboard-tab-content
position: absolute
left: 219px
top: 21px
width: 571px
height: 514px
padding: 50px
overflow-y: scroll
::-webkit-scrollbar
// So that the scrollbar doesn't go on top of the close button.
// Wish we could easily do this for Firefox.
display: none

View file

@ -0,0 +1,36 @@
.leaderboard-tab-view
h1
margin-top: -20px
color: rgb(254,188,68)
font-size: 30px
text-shadow: black 2px 2px 0, black -2px -2px 0, black 2px -2px 0, black -2px 2px 0, black 2px 0px 0, black 0px -2px 0, black -2px 0px 0, black 0px 2px 0
table
td
padding: 1px 2px
font-size: 16px
text-align: center
th
text-align: center
tbody
tr.viewable
cursor: pointer
.rank-cell
font-weight: bold
.name-col-cell
max-width: 150px
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
.hero-portrait-cell, .code-language-cell
background: transparent url(/images/common/code_languages/javascript_small.png) no-repeat center center
background-size: 30px 30px
height: 30px
width: 32px

View file

@ -68,8 +68,7 @@ block content
p(data-i18n="about.george_blurb")
| Businesser
a(href="http://scotterickson.info")
img(src="/images/pages/about/scott_small.png").img-thumbnail
img(src="/images/pages/about/scott_small.png").img-thumbnail
.team_bio
@ -122,5 +121,3 @@ block content
| Programmer
p(data-i18n="about.matt_blurb")
| Bicyclist

View file

@ -20,8 +20,7 @@ block content
| dozens of open source projects, and we love them. See
a(href="https://github.com/codecombat/codecombat/wiki/Archmage-Home", data-i18n="legal.archmage_wiki_url")
| our Archmage wiki
span
span(data-i18n="legal.opensource_description_suffix")
span.spl(data-i18n="legal.opensource_description_suffix")
| for a list of the software that makes this game possible.
hr
@ -43,13 +42,11 @@ block content
h4(data-i18n="legal.email_title")
| Email
p
span(data-i18n="legal.email_description_prefix")
span.spr(data-i18n="legal.email_description_prefix")
| We will not inundate you with spam. Through
span
a(href='/account/settings', data-i18n="legal.email_settings_url")
| your email settings
span
span(data-i18n="legal.email_description_suffix")
span.spl(data-i18n="legal.email_description_suffix")
| or through links in the emails we send,
| you can change your preferences and easily unsubscribe at any time.
h4(data-i18n="legal.cost_title")
@ -64,9 +61,8 @@ block content
| Contributor License Agreement
p
span(data-i18n="legal.contributor_description_prefix")
span.spr(data-i18n="legal.contributor_description_prefix")
| All contributions, both on the site and on our GitHub repository, are subject to our
span
a(href="/cla", data-i18n="legal.cla_url")
| CLA
span ,
@ -77,11 +73,10 @@ block content
| Code - MIT
p
span(data-i18n="legal.code_description_prefix")
span.spr(data-i18n="legal.code_description_prefix")
| All code owned by CodeCombat or hosted on codecombat.com,
| both in the GitHub repository or in the codecombat.com database,
| is licensed under the
span
a(href="http://opensource.org/licenses/MIT", data-i18n="legal.mit_license_url")
| MIT license
span .
@ -91,14 +86,11 @@ block content
h3(data-i18n="legal.art_title")
| Art/Music - Creative Commons
a(rel='license', href='http://creativecommons.org/licenses/by/4.0/').cc-license-link
img(alt='Creative Commons License', style='border-width: 0; margin-left: 10px', src='http://i.creativecommons.org/l/by/4.0/88x31.png')
p
span(data-i18n="legal.art_description_prefix")
p
span.spr(data-i18n="legal.art_description_prefix")
| All common content is available under the
span
a(href="http://creativecommons.org/licenses/by/4.0/", data-i18n="legal.cc_license_url")
a(href="https://creativecommons.org/licenses/by/4.0/", data-i18n="legal.cc_license_url")
| Creative Commons Attribution 4.0 International License
span .
span(data-i18n="legal.art_description_suffix")

View file

@ -9,7 +9,7 @@
.glyphicon.glyphicon-play
span(data-i18n="nav.play").home-text Levels
if isMultiplayerLevel
if isMultiplayerLevel && !observing
.multiplayer-area-container
.multiplayer-area
.multiplayer-label(data-i18n="play_level.control_bar_multiplayer")
@ -28,17 +28,19 @@ else
.buttons-area
button.btn.btn-inverse#game-menu-button(title="Show game menu")
.hamburger
span.icon-bar
span.icon-bar
span.icon-bar
span.game-menu-text(data-i18n="play_level.game_menu") Game Menu
if !observing
button.btn.btn-inverse#game-menu-button(title="Show game menu")
.hamburger
span.icon-bar
span.icon-bar
span.icon-bar
span.game-menu-text(data-i18n="play_level.game_menu") Game Menu
if spectateGame
button.btn.btn-xs.btn-inverse.banner#next-game-button(title="Next Game", data-i18n="play_level.next-game") Next game!
button.btn.btn-xs.btn-primary.banner#level-done-button(data-i18n="play_level.done") Done
if !observing
button.btn.btn-xs.btn-primary.banner#level-done-button(data-i18n="play_level.done") Done
if me.get('anonymous')
button.btn.btn-xs.btn-primary.banner#control-bar-sign-up-button(data-toggle='coco-modal', data-target='core/AuthModal', data-i18n="signup.sign_up")

View file

@ -68,6 +68,9 @@ block modal-footer-content
.sign-up-blurb(data-i18n="play_level.victory_sign_up_poke") Want to save your code? Create a free account!
button.btn.btn-illustrated.btn-warning.sign-up-button.btn-lg(data-dismiss="modal", data-i18n="play_level.victory_sign_up") Sign Up to Save Progress
else if !showHourOfCodeDoneButton && showLeaderboard
button.btn.btn-illustrated.btn-warning.leaderboard-button.btn-lg(data-dismiss="modal", data-i18n="leaderboard.view_other_solutions") View Other Solutions
button.btn.btn-illustrated.btn-lg.btn-warning.hide#saving-progress-label(disabled, data-i18n="play_level.victory_saving_progress") Saving Progress
.next-level-buttons

View file

@ -1,9 +1,10 @@
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)
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
if !observing
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

@ -0,0 +1,18 @@
.modal-dialog
.modal-content
img(src="/images/pages/play/modal/game-menu-background.png", draggable="false")#leaderboard-background
div#close-modal
span.glyphicon.glyphicon-remove
ul#leaderboard-nav.nav.nav-pills.nav-stacked
for submenu, index in submenus
li(class=index ? "" : "active")
a(href='#' + submenu.scoreType + '-' + submenu.timespan + '-view', data-toggle='tab')
.scoreType(data-i18n='leaderboard.' + submenu.scoreType.replace('-', '_'))= submenu.scoreType
.timespan(data-i18n='leaderboard.' + submenu.timespan)
.tab-content.leaderboard-tab-content
for submenu, index in submenus
.tab-pane(id=submenu.scoreType + '-' + submenu.timespan + '-view')
.leaderboard-tab-view

View file

@ -0,0 +1,34 @@
h1
span.spr(data-i18n="leaderboard.top") Top Players by
span(data-i18n="leaderboard.#{scoreType.replace('-', '_')}")
span.spr ,
span(data-i18n="leaderboard.#{timespan}")
if topScores
table.table.table-bordered.table-condensed.table-hover
thead
tr
th(colspan=4, data-i18n="general.player")
th(data-i18n="general.score")
th(data-i18n="general.when")
th
tbody
for row, rank in topScores
- var isMyRow = row.creator == me.id
- var viewable = rank >= 5 || me.isAdmin();
tr(class=isMyRow ? "success" : "" + (viewable ? " viewable" : ""), data-player-id=row.creator, data-session-id=row.session, title=viewable ? "View solution" : "Can't view top 5 solutions")
td.rank-cell= rank + 1
td.code-language-cell(style="background-image: url(/images/common/code_languages/#{row.codeLanguage}_small.png)" title=_.string.capitalize(row.codeLanguage))
td.hero-portrait-cell(style="background-image: url(/file/db/thang.type/#{row.hero}/portrait.png)")
td.name-col-cell= row.creatorName || "Anonymous"
td.score-cell= row.score
td.ago-cell= row.ago
td.viewable-cell
if viewable
.glyphicon.glyphicon-eye-open
else
.glyphicon.glyphicon-eye-close
else if loading
h3(data-i18n="common.loading")
else
h3 No scores yet.

View file

@ -6,69 +6,114 @@ block content
.span5
h2 CodeCombat for Teachers
h2(data-i18n="teachers.title") CodeCombat for Teachers
h3 Preparation
h3(data-i18n="teachers.preparation_title") Preparation
p CodeCombat is free to play for the core level progression and does not require students to sign up. We encourage teachers to
a(href="/play") play through the campaign
| to try it out, but the only thing you absolutely need to do to be ready is ensure students have access to a computer.
p
span.spr(data-i18n="teachers.preparation_1")
| CodeCombat is free to play for the core level progression
| and does not require students to sign up. We encourage teachers to
a(href="/play", data-i18n="teachers.preparation_play_campaign") play through the campaign
span.spl(data-i18n="teachers.preparation_2")
| to try it out, but the only thing you absolutely need to do
| to be ready is ensure students have access to a computer.
p It is not necessary for teachers to be comfortable with computer science concepts for students to have fun learning with CodeCombat.
p(data-i18n="teachers.preparation_3")
| It is not necessary for teachers to be comfortable with computer
| science concepts for students to have fun learning with CodeCombat.
h3 Is it violent?
h3(data-i18n="teachers.violent_title")
| Is it violent?
p We get this from teachers a lot due to our name. Although CodeCombat does contain cartoon violence, there is nothing graphic in either the visuals or language. If you are comfortable having your students play Angry Birds, you will be comfortable with CodeCombat.
p
span.spr(data-i18n="teachers.violent_1")
| We get this from teachers a lot due to our name. Although CodeCombat
| does contain cartoon violence, there is nothing graphic in either the
| visuals or language.
span(data-i18n="teachers.violent_2")
| If you are comfortable having your students play Angry Birds, you will
| be comfortable with CodeCombat.
h3 Is it for girls?
h3(data-i18n="teachers.for_girls_title")
| Is it for girls?
p There are three game modes in CodeCombat: building, puzzles, and combat. We have intentionally designed each to appeal to both boys and girls and think that the building and puzzle levels especially differentiate the game from violent triple A titles that repel female players.
p(data-i18n="teachers.for_girls_1")
| There are three game modes in CodeCombat: building, puzzles, and combat.
| We have intentionally designed each to appeal to both boys and girls and
| think that the building and puzzle levels especially differentiate the game
| from violent triple A titles that repel female players.
h3 What do we cover?
h3(data-i18n="teachers.what_cover_title")
| What do we cover?
p There are 20 levels in the Hour of Code tutorial that teach and reinforce 6 specific computer science concepts:
p(data-i18n="teachers.what_cover_1")
| There are 20 levels in the Hour of Code tutorial that teach and
| reinforce 6 specific computer science concepts:
ol
li
strong Formal notation
| - builds an understanding of the importance of syntax in programming.
strong.spr(data-i18n="teachers.what_cover_notation_1") Formal notation
span(data-i18n="teachers.what_cover_notation_2")
| - builds an understanding of the importance of syntax in programming.
li
strong Calling methods
| - familiarizes students with the syntax of object-oriented method calls.
strong.spr(data-i18n="teachers.what_cover_methods_1") Calling methods
span(data-i18n="teachers.what_cover_methods_2")
| - familiarizes students with the syntax of object-oriented method calls.
li
strong Parameters
| - trains how to pass parameters to functions.
strong.spr(data-i18n="teachers.what_cover_parameters_1") Parameters
span(data-i18n="teachers.what_cover_parameters_2")
| - trains how to pass parameters to functions.
li
strong Strings
| - teaches students about string notation and passing strings as parameters.
strong.spr(data-i18n="teachers.what_cover_strings_1") Strings
span(data-i18n="teachers.what_cover_strings_2")
| - teaches students about string notation and passing strings as parameters.
li
strong Loops
| - develops the abstraction of designing short programs with loops.
strong.spr(data-i18n="teachers.what_cover_loops_1") Loops
span(data-i18n="teachers.what_cover_loops_2")
| - develops the abstraction of designing short programs with loops.
li
strong Variables
| - adds the skill of referencing values that change over time.
strong.spr(data-i18n="teachers.what_cover_variables_1") Variables
span(data-i18n="teachers.what_cover_variables_2")
| - adds the skill of referencing values that change over time.
p Students may continue past level 20, depending on their speed and interest, to learn two additional concepts in later levels:
p(data-i18n="teachers.what_cover_2")
| Students may continue past level 20, depending on their
| speed and interest, to learn two additional concepts in later levels:
ol
li
strong Conditional logic
| - when and how to use if/else to control in-game outcomes.
strong.spr(data-i18n="teachers.what_cover_logic_1") Conditional logic
span(data-i18n="teachers.what_cover_logic_2")
| - when and how to use if/else to control in-game outcomes.
li
strong Handling player input
| - responding to input events to create a user interface.
strong.spr(data-i18n="teachers.what_cover_input_1") Handling player input
span(data-i18n="teachers.what_cover_input_2")
| - responding to input events to create a user interface.
h3 System Requirements
h3(data-i18n="teachers.sys_requirements_title") System Requirements
p Because CodeCombat is a game, it is more intensive for computers to run smoothly than video or written tutorials. We have optimized it to run quickly on all modern browsers and on older machines so that everyone can play. That said, here are our suggestions for getting the most out of your Hour of Code experience:
p(data-i18n="teachers.sys_requirements_1")
| Because CodeCombat is a game, it is more intensive for computers
| to run smoothly than video or written tutorials. We have optimized
| it to run quickly on all modern browsers and on older machines so
| that everyone can play. That said, here are our suggestions for getting
| the most out of your Hour of Code experience:
ul
li
strong Use newer versions of Chrome or Firefox.
| Although CodeCombat will work on browsers as old as IE9, the performance is not as good. Chrome is best.
strong.spr(data-i18n="teachers.sys_requirements_2") Use newer versions of Chrome or Firefox.
span(data-i18n="teachers.sys_requirements_3")
| Although CodeCombat will work on browsers as old as IE9, the
| performance is not as good. Chrome is best.
li
strong Use newer computers.
| Older computers, Chromebooks, and netbooks tend to have very few system resources, which makes for a less enjoyable experience. At least 2GB of RAM is required.
strong.spr(data-i18n="teachers.sys_requirements_4") Use newer computers.
span(data-i18n="teachers.sys_requirements_5")
| Older computers, Chromebooks, and netbooks tend to have very few
| system resources, which makes for a less enjoyable experience.
| At least 2GB of RAM is required.
li
strong Allow players to wear headphones/earbuds to hear the audio.
| We help players learn through voiceover and sound effects, which will make classrooms noisy and distracting.
strong.spr(data-i18n="teachers.sys_requirements_6") Allow players to wear headphones/earbuds to hear the audio.
span(data-i18n="teachers.sys_requirements_7")
| We help players learn through voiceover and sound effects, which
| will make classrooms noisy and distracting.

View file

@ -8,9 +8,9 @@ class SearchCollection extends Backbone.Collection
@url = "#{modelURL}?project="
if @projection?.length
@url += 'created,permissions'
@url += ',' + projected for projected in projection
@url += ',' + projected for projected in @projection
else @url += 'true'
@url += "&term=#{term}" if @term
@url += "&term=#{@term}" if @term
comparator: (a, b) ->
score = 0

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', 'replayable'
'tasks', 'helpVideos', 'replayable', 'scoreTypes'
]
subscriptions:

View file

@ -12,7 +12,7 @@ class VersionsViewCollection extends CocoCollection
initialize: (@url, @levelID, @model) ->
super()
@url = url + @levelID + '/versions'
@url = @url + @levelID + '/versions'
module.exports = class VersionsModal extends ModalView
template: template

View file

@ -11,6 +11,7 @@ MusicPlayer = require 'lib/surface/MusicPlayer'
storage = require 'core/storage'
AuthModal = require 'views/core/AuthModal'
SubscribeModal = require 'views/core/SubscribeModal'
LeaderboardModal = require 'views/play/modal/LeaderboardModal'
Level = require 'models/Level'
utils = require 'core/utils'
require 'vendor/three'
@ -205,6 +206,7 @@ module.exports = class CampaignView extends RootView
for nextLevelOriginal in level.nextLevels ? []
if nextLevel = _.find(@campaign.renderedLevels, original: nextLevelOriginal)
@createLine level.position, nextLevel.position
@showLeaderboard @options.justBeatLevel?.get('slug') if @options.showLeaderboard or true
@applyCampaignStyles()
@testParticles()
@ -217,6 +219,11 @@ module.exports = class CampaignView extends RootView
authModal.mode = 'signup'
@openModalView authModal
showLeaderboard: (levelSlug) ->
levelSlug ?= 'siege-of-stonehold' # Testing
leaderboardModal = new LeaderboardModal supermodel: @supermodel, levelSlug: levelSlug
@openModalView leaderboardModal
determineNextLevel: (levels) ->
foundNext = false
for level in levels

View file

@ -33,6 +33,7 @@ module.exports = class ControlBarView extends CocoView
@level = options.level
@levelID = @level.get('slug')
@spectateGame = options.spectateGame ? false
@observing = options.session.get('creator') isnt me.id
super options
if @level.get('type') in ['hero-ladder'] and me.isAdmin()
@isMultiplayerLevel = true
@ -66,6 +67,7 @@ module.exports = class ControlBarView extends CocoView
c.difficultyTitle = "#{$.i18n.t 'play.level_difficulty'}#{c.levelDifficulty}"
@lastDifficulty = c.levelDifficulty
c.spectateGame = @spectateGame
c.observing = @observing
@homeViewArgs = [{supermodel: if @hasReceivedMemoryWarning then null else @supermodel}]
if @level.get('type', true) in ['ladder', 'ladder-tutorial', 'hero-ladder']
levelID = @level.get('slug').replace /\-tutorial$/, ''

View file

@ -103,7 +103,7 @@ module.exports = class LevelHUDView extends CocoView
if @thang.id in ['Hero Placeholder', 'Hero Placeholder 1']
name = {knight: 'Tharin', captain: 'Anya', librarian: 'Hushbaum', sorcerer: 'Pender', 'potion-master': 'Omarn', samurai: 'Hattori', ninja: 'Amara'}[@thang.type] ? 'Hero'
else
name = if @thang.type then "#{@thang.id} - #{@thang.type}" else @thang.id
name = @thang.hudName or (if @thang.type then "#{@thang.id} - #{@thang.type}" else @thang.id)
utils.replaceText @$el.find('.thang-name'), name
props = @$el.find('.thang-props')
props.find('.prop').remove()

View file

@ -95,6 +95,7 @@ module.exports = class PlayLevelView extends RootView
@isEditorPreview = @getQueryVariable 'dev'
@sessionID = @getQueryVariable 'session'
@observing = @getQueryVariable 'observing'
@opponentSessionID = @getQueryVariable('opponent')
@opponentSessionID ?= @options.opponent
@ -109,7 +110,7 @@ module.exports = class PlayLevelView extends RootView
setTimeout f, 100
else
@load()
application.tracker?.trackEvent 'Started Level Load', category: 'Play Level', level: @levelID, label: @levelID, ['Google Analytics']
application.tracker?.trackEvent 'Started Level Load', category: 'Play Level', level: @levelID, label: @levelID, ['Google Analytics'] unless @observing
setLevel: (@level, givenSupermodel) ->
@supermodel.models = givenSupermodel.models
@ -134,8 +135,9 @@ module.exports = class PlayLevelView extends RootView
@loadEndTime = new Date()
loadDuration = @loadEndTime - @loadStartTime
console.debug "Level unveiled after #{(loadDuration / 1000).toFixed(2)}s"
application.tracker?.trackEvent 'Finished Level Load', category: 'Play Level', label: @levelID, level: @levelID, loadDuration: loadDuration, ['Google Analytics']
application.tracker?.trackTiming loadDuration, 'Level Load Time', @levelID, @levelID
unless @observing
application.tracker?.trackEvent 'Finished Level Load', category: 'Play Level', label: @levelID, level: @levelID, loadDuration: loadDuration, ['Google Analytics']
application.tracker?.trackTiming loadDuration, 'Level Load Time', @levelID, @levelID
# CocoView overridden methods ###############################################
@ -147,7 +149,7 @@ module.exports = class PlayLevelView extends RootView
afterRender: ->
super()
window.onPlayLevelViewLoaded? @ # still a hack
@insertSubView @loadingView = new LevelLoadingView autoUnveil: @options.autoUnveil, level: @levelLoader?.level ? @level # May not have @level loaded yet
@insertSubView @loadingView = new LevelLoadingView autoUnveil: @options.autoUnveil or @observing, level: @levelLoader?.level ? @level # May not have @level loaded yet
@$el.find('#level-done-button').hide()
$('body').addClass('is-playing')
$('body').bind('touchmove', false) if @isIPadApp()
@ -233,7 +235,7 @@ 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
@insertSubView @tome = new TomeView levelID: @levelID, session: @session, otherSession: @otherSession, thangs: @world.thangs, supermodel: @supermodel, level: @level, observing: @observing
@insertSubView new LevelPlaybackView session: @session, level: @level
@insertSubView new GoalsView {}
@insertSubView new LevelFlagsView levelID: @levelID, world: @world if @$el.hasClass 'flags'
@ -283,7 +285,7 @@ module.exports = class PlayLevelView extends RootView
return unless @levelLoader.progress() is 1 # double check, since closing the guide may trigger this early
# Save latest level played.
if not (@levelLoader.level.get('type') in ['ladder', 'ladder-tutorial'])
if not @observing and not (@levelLoader.level.get('type') in ['ladder', 'ladder-tutorial'])
me.set('lastLevel', @levelID)
me.save()
application.tracker?.identify()
@ -321,7 +323,7 @@ module.exports = class PlayLevelView extends RootView
if @otherSession and not (@level.get('type', true) in ['hero', 'hero-ladder', 'hero-coop'])
# TODO: colorize name and cloud by team, colorize wizard by user's color config
@surface.createOpponentWizard id: @otherSession.get('creator'), name: @otherSession.get('creatorName'), team: @otherSession.get('team'), levelSlug: @level.get('slug'), codeLanguage: @otherSession.get('submittedCodeLanguage')
if @isEditorPreview
if @isEditorPreview or @observing
@loadingView.startUnveiling()
@loadingView.unveil()
@ -337,7 +339,7 @@ module.exports = class PlayLevelView extends RootView
Backbone.Mediator.publish 'playback:real-time-playback-waiting', {}
@realTimeMultiplayerContinueGame @options.realTimeMultiplayerSessionID
# TODO: Is it possible to create a Mongoose ObjectId for 'ls', instead of the string returned from get()?
application.tracker?.trackEvent 'Started Level', category:'Play Level', levelID: @levelID, ls: @session?.get('_id')
application.tracker?.trackEvent 'Started Level', category:'Play Level', levelID: @levelID, ls: @session?.get('_id') unless @observing
playAmbientSound: ->
return if @destroyed
@ -414,7 +416,7 @@ module.exports = class PlayLevelView extends RootView
return if @victorySeen
@victorySeen = true
victoryTime = (new Date()) - @loadEndTime
if victoryTime > 10 * 1000 # Don't track it if we're reloading an already-beaten level
if not @observing and victoryTime > 10 * 1000 # Don't track it if we're reloading an already-beaten level
application.tracker?.trackEvent 'Saw Victory',
category: 'Play Level'
level: @level.get('name')
@ -436,12 +438,12 @@ module.exports = class PlayLevelView extends RootView
@tome.reloadAllCode()
Backbone.Mediator.publish 'level:restarted', {}
$('#level-done-button', @$el).hide()
application.tracker?.trackEvent 'Confirmed Restart', category: 'Play Level', level: @level.get('name'), label: @level.get('name')
application.tracker?.trackEvent 'Confirmed Restart', category: 'Play Level', level: @level.get('name'), label: @level.get('name') unless @observing
onInfiniteLoop: (e) ->
return unless e.firstWorld
@openModalView new InfiniteLoopModal()
application.tracker?.trackEvent 'Saw Initial Infinite Loop', category: 'Play Level', level: @level.get('name'), label: @level.get('name')
application.tracker?.trackEvent 'Saw Initial Infinite Loop', category: 'Play Level', level: @level.get('name'), label: @level.get('name') unless @observing
onHighlightDOM: (e) -> @highlightElement e.selector, delay: e.delay, sides: e.sides, offset: e.offset, rotation: e.rotation
@ -525,6 +527,7 @@ module.exports = class PlayLevelView extends RootView
# TODO: Show a victory dialog specific to hero-ladder level
if @goalManager.checkOverallStatus() is 'success' and not @options.realTimeMultiplayerSessionID?
showModalFn = -> Backbone.Mediator.publish 'level:show-victory', showModal: true
@session.recordScores @world.scores, @level
if @level.get 'replayable'
@session.increaseDifficulty showModalFn
else

View file

@ -23,6 +23,7 @@ module.exports = class HeroVictoryModal extends ModalView
events:
'click #continue-button': 'onClickContinue'
'click .leaderboard-button': 'onClickLeaderboard'
'click .return-to-ladder-button': 'onClickReturnToLadder'
'click .sign-up-button': 'onClickSignupButton'
@ -145,6 +146,12 @@ module.exports = class HeroVictoryModal extends ModalView
# Show the "I'm done" button between 30 - 120 minutes if they definitely came from Hour of Code
c.showHourOfCodeDoneButton = me.get('hourOfCode') and showDone
if @level.get('scoreTypes')?.length
lg = me.getLeaderboardsGroup()
c.showLeaderboard = lg is 'always'
c.showLeaderboard = true if me.level() >= 3 and lg.group is 'early'
c.showLeaderboard = true if me.level() >= 5 and lg.group is 'late'
return c
afterRender: ->
@ -322,11 +329,18 @@ module.exports = class HeroVictoryModal extends ModalView
link += '/' + nextCampaign unless nextCampaign is 'dungeon'
link
onClickContinue: (e) ->
onClickContinue: (e, extraOptions=null) ->
@playSound 'menu-button-click'
nextLevelLink = @getNextLevelLink()
# Preserve the supermodel as we navigate back to the world map.
Backbone.Mediator.publish 'router:navigate', route: nextLevelLink, viewClass: require('views/play/CampaignView'), viewArgs: [{supermodel: if @options.hasReceivedMemoryWarning then null else @supermodel}, @getNextLevelCampaign()]
options =
justBeatLevel: @level
supermodel: if @options.hasReceivedMemoryWarning then null else @supermodel
_.merge options, extraOptions if extraOptions
Backbone.Mediator.publish 'router:navigate', route: nextLevelLink, viewClass: require('views/play/CampaignView'), viewArgs: [options, @getNextLevelCampaign()]
onClickLeaderboard: (e) ->
@onClickContinue e, showLeaderboard: true
onClickReturnToLadder: (e) ->
@playSound 'menu-button-click'

View file

@ -27,6 +27,7 @@ module.exports = class CastButtonView extends CocoView
@spells = options.spells
@castShortcut = '⇧↵'
@updateReplayabilityInterval = setInterval @updateReplayability, 1000
@observing = options.session.get('creator') isnt me.id
destroy: ->
clearInterval @updateReplayabilityInterval
@ -40,6 +41,7 @@ module.exports = class CastButtonView extends CocoView
castRealTimeShortcutVerbose = (if @isMac() then 'Cmd' else 'Ctrl') + '+' + castShortcutVerbose
context.castVerbose = castShortcutVerbose + ': ' + $.i18n.t('keyboard_shortcuts.run_code')
context.castRealTimeVerbose = castRealTimeShortcutVerbose + ': ' + $.i18n.t('keyboard_shortcuts.run_real_time')
context.observing = @observing
context
afterRender: ->
@ -73,6 +75,7 @@ module.exports = class CastButtonView extends CocoView
@updateReplayability()
onDoneButtonClick: (e) ->
@options.session.recordScores @world.scores, @options.level
Backbone.Mediator.publish 'level:show-victory', showModal: true
onSpellChanged: (e) ->
@ -97,6 +100,7 @@ module.exports = class CastButtonView extends CocoView
@playSound 'cast-end', 0.5
@hasCastOnce = true
@updateCastButton()
@world = e.world
onNewGoalStates: (e) ->
winnable = e.overallStatus is 'success'

View file

@ -15,6 +15,7 @@ module.exports = class Spell
@otherSession = options.otherSession
@spectateView = options.spectateView
@spectateOpponentCodeLanguage = options.spectateOpponentCodeLanguage
@observing = options.observing
@supermodel = options.supermodel
@skipProtectAPI = options.skipProtectAPI
@worker = options.worker
@ -189,6 +190,7 @@ module.exports = class Spell
return true if @spectateView # Use transpiled code for both teams if we're just spectating.
return true if @isEnemySpell() # Use transpiled for enemy spells.
# Players without permissions can't view the raw code.
return false if @observing and @levelType is 'hero'
return true if @session.get('creator') isnt me.id and not (me.isAdmin() or 'employer' in me.get('permissions', true))
false

View file

@ -69,6 +69,7 @@ module.exports = class SpellView extends CocoView
@writable = false unless me.team in @spell.permissions.readwrite # TODO: make this do anything
@highlightCurrentLine = _.throttle @highlightCurrentLine, 100
$(window).on 'resize', @onWindowResize
@observing = @session.get('creator') isnt me.id
afterRender: ->
super()
@ -119,14 +120,15 @@ module.exports = class SpellView extends CocoView
name: 'run-code'
bindKey: {win: 'Shift-Enter|Ctrl-Enter', mac: 'Shift-Enter|Command-Enter|Ctrl-Enter'}
exec: -> Backbone.Mediator.publish 'tome:manual-cast', {}
addCommand
name: 'run-code-real-time'
bindKey: {win: 'Ctrl-Shift-Enter', mac: 'Command-Shift-Enter|Ctrl-Shift-Enter'}
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}
unless @observing
addCommand
name: 'run-code-real-time'
bindKey: {win: 'Ctrl-Shift-Enter', mac: 'Command-Shift-Enter|Ctrl-Shift-Enter'}
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'}

View file

@ -134,6 +134,7 @@ module.exports = class TomeView extends CocoView
language: language
spectateView: @options.spectateView
spectateOpponentCodeLanguage: @options.spectateOpponentCodeLanguage
observing: @options.observing
levelID: @options.levelID
level: @options.level

View file

@ -0,0 +1,56 @@
ModalView = require 'views/core/ModalView'
template = require 'templates/play/modal/leaderboard-modal'
LeaderboardTabView = require 'views/play/modal/LeaderboardTabView'
Level = require 'models/Level'
module.exports = class LeaderboardModal extends ModalView
id: 'leaderboard-modal'
template: template
instant: true
timespans: ['day', 'week', 'all']
subscriptions: {}
events:
'shown.bs.tab #leaderboard-nav a': 'onTabShown'
'click #close-modal': 'hide'
constructor: (options) ->
super options
@levelSlug = @options.levelSlug
@level = @supermodel.loadModel(new Level({_id: @levelSlug}, {project: ['name', 'i18n', 'scoreType', 'original']}), 'level').model
getRenderData: (c) ->
c = super c
c.submenus = []
for scoreType in @level.get('scoreTypes') ? []
for timespan in @timespans
c.submenus.push scoreType: scoreType, timespan: timespan
c
afterRender: ->
super()
return unless @supermodel.finished()
for scoreType, scoreTypeIndex in @level.get('scoreTypes') ? []
for timespan, timespanIndex in @timespans
submenuView = new LeaderboardTabView scoreType: scoreType, timespan: timespan, level: @level
@insertSubView submenuView, @$el.find "##{scoreType}-#{timespan}-view .leaderboard-tab-view"
if scoreTypeIndex + timespanIndex is 0
submenuView.$el.parent().addClass 'active'
submenuView.onShown?()
@playSound 'game-menu-open'
@$el.find('.nano:visible').nanoScroller()
onTabShown: (e) ->
@playSound 'game-menu-tab-switch'
tabChunks = e.target.hash.substring(1).split '-'
scoreType = tabChunks[0 ... tabChunks.length - 2].join '-'
timespan = tabChunks[tabChunks.length - 2]
subview = _.find @subviews, scoreType: scoreType, timespan: timespan
subview.onShown?()
otherSubview.onHidden?() for subviewKey, otherSubview of @subviews when otherSubview isnt subview
onHidden: ->
super()
subview.onHidden?() for subviewKey, subview of @subviews
@playSound 'game-menu-close'

View file

@ -0,0 +1,75 @@
CocoView = require 'views/core/CocoView'
template = require 'templates/play/modal/leaderboard-tab-view'
CocoCollection = require 'collections/CocoCollection'
LevelSession = require 'models/LevelSession'
class TopScoresCollection extends CocoCollection
url: ''
model: LevelSession
constructor: (@level, @scoreType, @timespan) ->
super()
@url = "/db/level/#{@level.get('original')}/top_scores/#{@scoreType}/#{@timespan}"
module.exports = class LeaderboardTabView extends CocoView
template: template
className: 'leaderboard-tab-view'
events:
'click tbody tr.viewable': 'onClickRow'
constructor: (options) ->
super options
@level = @options.level
@scoreType = @options.scoreType ? 'time'
@timespan = @options.timespan
destroy: ->
super()
getRenderData: ->
c = super()
c.scoreType = @scoreType
c.timespan = @timespan
c.topScores = @formatTopScores()
c.loading = not @sessions or @sessions.loading
c._ = _
c
afterRender: ->
super()
formatTopScores: ->
return [] unless @sessions?.models
rows = []
for s in @sessions.models
row = {}
score = _.find s.get('state').topScores, type: @scoreType
row.ago = moment(new Date(score.date)).fromNow()
row.score = @formatScore score
row.creatorName = s.get 'creatorName'
row.creator = s.get 'creator'
row.session = s.id
row.codeLanguage = s.get 'codeLanguage'
row.hero = s.get('heroConfig')?.thangType
row.inventory = s.get('heroConfig')?.inventory
rows.push row
rows
formatScore: (score) ->
switch score.type
when 'time' then -score.score.toFixed(2) + 's'
when 'damage-taken' then -Math.round score.score
when 'damage-dealt', 'gold-collected', 'difficulty' then Math.round score.score
else score.score
onShown: ->
return if @hasShown
@hasShown = true
topScores = new TopScoresCollection @level, @scoreType, @timespan
@sessions = @supermodel.loadCollection(topScores, 'sessions', null, 0).model
onClickRow: (e) ->
sessionID = $(e.target).closest('tr').data 'session-id'
url = "/play/level/#{@level.get('slug')}?session=#{sessionID}&observing=true"
window.open url, '_blank'

View file

@ -59,6 +59,7 @@ LevelHandler = class LevelHandler extends Handler
'campaign'
'replayable'
'buildTime'
'scoreTypes'
]
postEditableProperties: ['name']
@ -77,6 +78,7 @@ LevelHandler = class LevelHandler extends Handler
return @checkExistence(req, res, args[0]) if args[1] is 'exists'
return @getPlayCountsBySlugs(req, res) if args[1] is 'play_counts'
return @getLevelPlaytimesBySlugs(req, res) if args[1] is 'playtime_averages'
return @getTopScores(req, res, args[0], args[2], args[3]) if args[1] is 'top_scores'
super(arguments...)
fetchLevelByIDAndHandleErrors: (id, req, res, callback) ->
@ -400,4 +402,33 @@ LevelHandler = class LevelHandler extends Handler
@levelPlaytimesCache[cacheKey] = playtimes
@sendSuccess res, playtimes
getTopScores: (req, res, levelOriginal, scoreType, timespan) ->
query =
'level.original': levelOriginal
'state.topScores.type': scoreType
now = new Date()
if timespan is 'day'
since = new Date now - 1 * 86400 * 1000
else if timespan is 'week'
since = new Date now - 7 * 86400 * 1000
if since
query['state.topScores.date'] = $gt: since.toISOString()
sort =
'state.topScores.score': -1
select = ['state.topScores', 'creatorName', 'creator', 'codeLanguage', 'heroConfig']
query = Session
.find(query)
.limit(20)
.sort(sort)
.select(select.join ' ')
query.exec (err, resultSessions) =>
return @sendDatabaseError(res, err) if err
resultSessions ?= []
@sendSuccess res, resultSessions
module.exports = new LevelHandler()

View file

@ -1,5 +1,3 @@
# TODO: not updated since rename from level_instance, or since we redid how all models are done; probably busted
mongoose = require 'mongoose'
plugins = require '../../plugins/plugins'
AchievablePlugin = require '../../plugins/achievements'
@ -24,6 +22,7 @@ LevelSessionSchema.index({submitted: 1}, {sparse: true})
LevelSessionSchema.index({team: 1}, {sparse: true})
LevelSessionSchema.index({totalScore: 1}, {sparse: true})
LevelSessionSchema.index({user: 1, changed: -1}, {name: 'last played index', sparse: true})
LevelSessionSchema.index({'level.original': 1, 'state.topScores.type': 1, 'state.topScores.date': -1, 'state.topScores.score': -1}, {name: 'top scores index', sparse: true})
LevelSessionSchema.plugin(plugins.PermissionsPlugin)
LevelSessionSchema.plugin(AchievablePlugin)
@ -65,7 +64,8 @@ LevelSessionSchema.statics.privateProperties = ['code', 'submittedCode', 'unsubs
LevelSessionSchema.statics.editableProperties = ['multiplayer', 'players', 'code', 'codeLanguage', 'completed', 'state',
'levelName', 'creatorName', 'levelID', 'screenshot',
'chat', 'teamSpells', 'submitted', 'submittedCodeLanguage',
'unsubscribed', 'playtime', 'heroConfig', 'team', 'transpiledCode']
'unsubscribed', 'playtime', 'heroConfig', 'team', 'transpiledCode',
'browser']
LevelSessionSchema.statics.jsonSchema = jsonschema
module.exports = LevelSession = mongoose.model('level.session', LevelSessionSchema, 'level.sessions')

View file

@ -15,7 +15,10 @@ class LevelSessionHandler extends Handler
formatEntity: (req, document) ->
documentObject = super(req, document)
if req.user?.isAdmin() or req.user?.id is document.creator or ('employer' in (req.user?.get('permissions') ? []))
if req.user?.isAdmin() or
req.user?.id is document.creator or
('employer' in (req.user?.get('permissions') ? [])) or
!document.submittedCode # TODO: only allow leaderboard access to non-top-5 solutions
return documentObject
else
return _.omit documentObject, @privateProperties
@ -47,8 +50,9 @@ class LevelSessionHandler extends Handler
@sendSuccess res, documents
hasAccessToDocument: (req, document, method=null) ->
return true if req.method is 'GET' and document.get('submitted')
return true if ('employer' in (req.user?.get('permissions') ? [])) and (method ? req.method).toLowerCase() is 'get'
get = (method ? req.method).toLowerCase() is 'get'
return true if get and document.get('submitted')
return true if get and ('employer' in (req.user?.get('permissions') ? []))
super(arguments...)
getCodeLanguageCounts: (req, res) ->