Merge pull request from codecombat/master

Update CodeCombat
This commit is contained in:
Roy Xue 2014-05-06 14:09:42 +08:00
commit 03e5d1ca50
27 changed files with 270 additions and 179 deletions

View file

@ -1,4 +1,4 @@
{backboneFailure, genericFailure} = require 'lib/errors'
{backboneFailure, genericFailure, parseServerError} = require 'lib/errors'
User = require 'models/User'
storage = require 'lib/storage'
BEEN_HERE_BEFORE_KEY = 'beenHereBefore'
@ -16,7 +16,14 @@ init = ->
module.exports.createUser = (userObject, failure=backboneFailure, nextURL=null) ->
user = new User(userObject)
user.save({}, {
error: failure,
error: (model,jqxhr,options) ->
error = parseServerError(jqxhr.responseText)
property = error.property if error.property
if jqxhr.status is 409 and property is 'name'
anonUserObject = _.omit(userObject, 'name')
module.exports.createUser anonUserObject, failure, nextURL
else
genericFailure(jqxhr)
success: -> if nextURL then window.location.href = nextURL else window.location.reload()
})

View file

@ -103,10 +103,8 @@ module.exports.getConflicts = (headDeltas, pendingDeltas) ->
pendingPathMap = groupDeltasByAffectingPaths(pendingDeltas)
paths = _.keys(headPathMap).concat(_.keys(pendingPathMap))
# Here's my thinking:
# A) Conflicts happen when one delta path is a substring of another delta path
# B) A delta from one self-consistent group cannot conflict with another
# So, sort the paths, which will naturally make conflicts adjacent,
# Here's my thinking: conflicts happen when one delta path is a substring of another delta path
# So, sort paths from both deltas together, which will naturally make conflicts adjacent,
# and if one is identified, one path is from the headDeltas, the other is from pendingDeltas
# This is all to avoid an O(nm) brute force search.
@ -141,7 +139,27 @@ groupDeltasByAffectingPaths = (deltas) ->
delta: delta
path: (item.toString() for item in path).join('/')
}
_.groupBy metaDeltas, 'path'
map = _.groupBy metaDeltas, 'path'
# Turns out there are cases where a single delta can include paths
# that 'conflict' with each other, ie one is a substring of the other
# because of moved indices. To handle this case, go through and prune
# out all deeper paths that conflict with more shallow paths, so
# getConflicts path checking works properly.
paths = _.keys(map)
return map unless paths.length
paths.sort()
prunedMap = {}
previousPath = paths[0]
for path, i in paths
continue if i is 0
continue if path.startsWith previousPath
prunedMap[path] = map[path]
previousPath = path
prunedMap
module.exports.pruneConflictsFromDelta = (delta, conflicts) ->
# the jsondiffpatch delta mustn't include any dangling nodes,

View file

@ -237,12 +237,12 @@ module.exports = class GoalManager extends CocoClass
# saveThangs: by default we would want to save all the Thangs, which means that we would want none of them to be "done"
numNeeded = _.size(stateThangs) - Math.min((goal.howMany ? 1), _.size stateThangs) + 1
numDone = _.filter(stateThangs).length
#console.log "needed", numNeeded, "done", numDone, "of total", _.size(stateThangs), "with how many", goal.howMany, "and stateThangs", stateThangs
console.log "needed", numNeeded, "done", numDone, "of total", _.size(stateThangs), "with how many", goal.howMany, "and stateThangs", stateThangs
return unless numDone >= numNeeded
return if state.status and not success # already failed it; don't wipe keyframe
state.status = if success then "success" else "failure"
state.keyFrame = frameNumber
#console.log goalID, "became", success, "on frame", frameNumber, "with overallStatus", @checkOverallStatus true
console.log goalID, "became", success, "on frame", frameNumber, "with overallStatus", @checkOverallStatus true
if overallStatus = @checkOverallStatus true
matchedGoals = (_.find(@goals, {id: goalID}) for goalID, goalState of @goalStates when goalState.status is overallStatus)
mostEagerGoal = _.min matchedGoals, 'worldEndsAfter'

View file

@ -246,6 +246,7 @@
multiplayer_hint_label: "Hint:"
multiplayer_hint: " Click the link to select all, then press ⌘-C or Ctrl-C to copy the link."
multiplayer_coming_soon: "More multiplayer features to come!"
multiplayer_sign_in_leaderboard: "Sign in or create an account and get your solution on the leaderboard."
guide_title: "Guide"
tome_minion_spells: "Your Minions' Spells"
tome_read_only_spells: "Read-Only Spells"
@ -710,3 +711,4 @@
user_names: "User Names"
files: "Files"
top_simulators: "Top Simulators"
source_document: "Source Document"

View file

@ -15,9 +15,9 @@ module.exports = nativeDescription: "Nederlands", englishDescription: "Dutch", t
fork: "Fork"
play: "Spelen"
retry: "Probeer opnieuw"
# watch: "Watch"
# unwatch: "Unwatch"
# submit_patch: "Submit Patch"
watch: "Volgen"
unwatch: "Ontvolgen"
submit_patch: "Correctie Opsturen"
units:
second: "seconde"
@ -36,11 +36,11 @@ module.exports = nativeDescription: "Nederlands", englishDescription: "Dutch", t
nav:
play: "Levels"
# community: "Community"
community: "Gemeenschap"
editor: "Editor"
blog: "Blog"
forum: "Forum"
# account: "Account"
account: "Lidmaatschap"
admin: "Administrator"
home: "Home"
contribute: "Bijdragen"
@ -152,7 +152,7 @@ module.exports = nativeDescription: "Nederlands", englishDescription: "Dutch", t
autosave: "Aanpassingen Automatisch Opgeslagen"
me_tab: "Ik"
picture_tab: "Afbeelding"
# upload_picture: "Upload a picture"
upload_picture: "Je afbeelding opsturen"
wizard_tab: "Tovenaar"
password_tab: "Wachtwoord"
emails_tab: "Emails"
@ -163,12 +163,12 @@ module.exports = nativeDescription: "Nederlands", englishDescription: "Dutch", t
email_subscriptions: "E-mail Abonnementen"
email_announcements: "Aankondigingen"
email_announcements_description: "Verkrijg emails over het laatste nieuws en de ontwikkelingen bij CodeCombat."
email_notifications: "Notificaties"
# email_notifications_summary: "Controls for personalized, automatic email notifications related to your CodeCombat activity."
# email_any_notes: "Any Notifications"
# email_any_notes_description: "Disable to stop all activity notification emails."
# email_recruit_notes: "Job Opportunities"
# email_recruit_notes_description: "If you play really well, we may contact you about getting you a (better) job."
email_notifications: "Meldingen"
email_notifications_summary: "Instellingen voor gepersonaliseerde, automatische meldingen via e-mail omtrent je activiteit op CodeCombat."
email_any_notes: "Alle Meldingen"
email_any_notes_description: "Zet alle activiteit-meldingen via e-mail af."
email_recruit_notes: "Job Aanbiedingen"
email_recruit_notes_description: "Als je zeer goed speelt, zouden we je wel eens kunnen contacteren om je een (betere) job aan te bieden."
contributor_emails: "Medewerker Klasse emails"
contribute_prefix: "We zoeken mensen om met ons te komen feesten! Bekijk de "
contribute_page: "bijdragepagina"
@ -180,8 +180,8 @@ module.exports = nativeDescription: "Nederlands", englishDescription: "Dutch", t
job_profile: "Job Profiel"
job_profile_approved: "Jouw job profiel werd goedgekeurd door CodeCombat. Werkgevers zullen het kunnen bekijken totdat je het inactief zet of als er geen verandering in komt voor vier weken."
job_profile_explanation: "Hey! Vul dit in en we zullen je contacteren om je een job als softwareontwikkelaar te helpen vinden."
# sample_profile: "See a sample profile"
# view_profile: "View Your Profile"
sample_profile: "Bekijk een voorbeeld kandidaat-profiel"
view_profile: "Bekijk je eigen kandidaat-profiel"
account_profile:
edit_settings: "Instellingen Aanpassen"
@ -199,7 +199,7 @@ module.exports = nativeDescription: "Nederlands", englishDescription: "Dutch", t
employers:
want_to_hire_our_players: "Wil je expert CodeCombat spelers aanwerven? "
# see_candidates: "Click here to see our candidates"
see_candidates: "Klik om je kandidaten te zien"
candidates_count_prefix: "Momenteel hebben we "
candidates_count_many: "veel"
candidates_count_suffix: "zeer getalenteerde en ervaren ontwikkelaars die werk zoeken."
@ -210,8 +210,8 @@ module.exports = nativeDescription: "Nederlands", englishDescription: "Dutch", t
candidate_top_skills: "Beste vaardigheden"
candidate_years_experience: "Jaren ervaring"
candidate_last_updated: "Laatst aangepast"
# candidate_approved: "Us?"
# candidate_active: "Them?"
candidate_approved: "Wij?"
candidate_active: "Zij?"
play_level:
level_load_error: "Level kon niet geladen worden: "
@ -257,7 +257,7 @@ module.exports = nativeDescription: "Nederlands", englishDescription: "Dutch", t
tome_select_spell: "Selecteer een Spreuk"
tome_select_a_thang: "Selecteer Iemand voor "
tome_available_spells: "Beschikbare spreuken"
hud_continue: "Ga verder (druk shift-space)"
hud_continue: "Ga verder (druk shift-spatie)"
spell_saved: "Spreuk Opgeslagen"
skip_tutorial: "Overslaan (esc)"
editor_config: "Editor Configuratie"
@ -268,7 +268,7 @@ module.exports = nativeDescription: "Nederlands", englishDescription: "Dutch", t
editor_config_keybindings_default: "Standaard (Ace)"
editor_config_keybindings_description: "Voeg extra shortcuts toe van de gebruikelijke editors."
editor_config_invisibles_label: "Toon onzichtbare"
editor_config_invisibles_description: "Toon onzichtbare whitespace karakters."
editor_config_invisibles_description: "Toon onzichtbare (spatie) karakters."
editor_config_indentguides_label: "Toon inspringing regels"
editor_config_indentguides_description: "Toon verticale hulplijnen om de zichtbaarheid te verbeteren."
editor_config_behaviors_label: "Slim gedrag"
@ -288,9 +288,9 @@ module.exports = nativeDescription: "Nederlands", englishDescription: "Dutch", t
tip_debugging_program: "Als debuggen het proces is om bugs te verwijderen, dan moet programmeren het proces zijn om ze erin te stoppen. - Edsger W. Dijkstra"
tip_forums: "Ga naar de forums en vertel ons wat je denkt!"
tip_baby_coders: "Zelfs babies zullen in de toekomst een Tovenaar zijn."
tip_morale_improves: "Het spel zal blijven laden tot de moreel verbeterd."
tip_morale_improves: "Het spel zal blijven laden tot de moreel verbetert."
tip_all_species: "Wij geloven in gelijke kansen voor alle wezens om te leren programmeren."
# tip_reticulating: "Reticulating spines."
tip_reticulating: "Paden aan het verknopen."
tip_harry: "Je bent een tovenaar, "
tip_great_responsibility: "Met een groots talent voor programmeren komt een grootse debug verantwoordelijkheid."
tip_munchkin: "Als je je groentjes niet opeet zal een munchkin je ontvoeren terwijl je slaapt."
@ -320,12 +320,12 @@ module.exports = nativeDescription: "Nederlands", englishDescription: "Dutch", t
lg_title: "Laatste Spelletjes"
clas: "CLAs"
# community:
# level_editor: "Level Editor"
# main_title: "CodeCombat Community"
# facebook: "Facebook"
# twitter: "Twitter"
# gplus: "Google+"
community:
level_editor: "Level Bewerker"
main_title: "CodeCombat Gemeenschap"
facebook: "Facebook"
twitter: "Twitter"
gplus: "Google+"
editor:
main_title: "CodeCombat Editors"
@ -336,7 +336,7 @@ module.exports = nativeDescription: "Nederlands", englishDescription: "Dutch", t
thang_description: "Maak eenheden, beschrijf hun standaard logica, graphics en audio. Momenteel is enkel het importeren van vector graphics geëxporteerd uit Flash ondersteund."
level_title: "Level Editor"
level_description: "Bevat de benodigdheden om scripts te schrijven, audio te uploaden en aangepaste logica te creëren om alle soorten levels te maken. Het is alles wat wij zelf ook gebruiken!"
# got_questions: "Questions about using the CodeCombat editors?"
got_questions: "Heb je vragen over het gebruik van de CodeCombat editors?"
contact_us: "contacteer ons!"
hipchat_prefix: "Je kan ons ook vinden in ons"
hipchat_url: "(Engelstalig) HipChat kanaal."
@ -378,9 +378,9 @@ module.exports = nativeDescription: "Nederlands", englishDescription: "Dutch", t
new_article_title: "Maak een Nieuw Artikel"
new_thang_title: "Maak een Nieuw Thang Type"
new_level_title: "Maak een Nieuw Level"
# new_article_title_signup: "Sign Up to Create a New Article"
# new_thang_title_signup: "Sign Up to Create a New Thang Type"
# new_level_title_signup: "Sign Up to Create a New Level"
new_article_title_signup: "Meld je aan om een Nieuw Artikel te maken"
new_thang_title_signup: "Meld je aan op een Nieuw Thang Type te maken"
new_level_title_signup: "Meld je aan om een Nieuw Level te maken"
article_search_title: "Zoek Artikels Hier"
thang_search_title: "Zoek Thang Types Hier"
level_search_title: "Zoek Levels Hier"
@ -650,13 +650,13 @@ module.exports = nativeDescription: "Nederlands", englishDescription: "Dutch", t
simple_ai: "Simpele AI"
warmup: "Opwarming"
vs: "tegen"
# friends_playing: "Friends Playing"
# sign_up_for_friends: "Sign up to play with your friends!"
# social_connect_blurb: "Connect and play against your friends!"
# invite_friends_to_battle: "Invite your friends to join you in battle!"
# fight: "Fight!"
# watch_victory: "Watch your victory"
# defeat_the: "Defeat the"
friends_playing: "Spelende Vrienden"
sign_up_for_friends: "Meld je aan om met je vrienden te spelen!"
social_connect_blurb: "Koppel je sociaal netwerk om tegen je vrienden te spelen!"
invite_friends_to_battle: "Nodig je vrienden uit om deel te nemen aan het gevecht!"
fight: "Aanvallen!"
watch_victory: "Aanschouw je overwinning!"
defeat_the: "Versla de"
multiplayer_launch:
introducing_dungeon_arena: "Introductie van Dungeon Arena"
@ -664,7 +664,7 @@ module.exports = nativeDescription: "Nederlands", englishDescription: "Dutch", t
to_battle: "Naar het slagveld, ontwikkelaars!"
modern_day_sorcerer: "Kan jij programmeren? Dat is pas stoer. Jij bent een moderne tovenaar! Is het niet tijd dat je jouw magische krachten gebruikt voor het besturen van jou minions in het slagveld? En nee, we praten hier niet over robots."
arenas_are_here: "CodeCombat's kop aan kop multiplayer arena's zijn er."
ladder_explanation: "Kies jouw helden, betover jouw mensen of ogre legers, en beklim jouw weg naar de top in de ladder, door het verslagen van vriend en vijand. Daag nu je vrienden uit in de multiplayer programmeer arena's en verdien eeuwige roem. Indien je creatief bent, kan je zelfs"
ladder_explanation: "Kies jouw helden, betover jouw mensen of ork-legers, en beklim jouw weg naar de top in de ladder, door het verslagen van vriend en vijand. Daag nu je vrienden uit in de multiplayer programmeer arena's en verdien eeuwige roem. Indien je creatief bent, kan je zelfs"
fork_our_arenas: "onze arenas forken"
create_worlds: "en jouw eigen werelden creëren."
javascript_rusty: "Jouw JavaScript is een beetje roestig? Wees niet bang, er is een"
@ -699,14 +699,14 @@ module.exports = nativeDescription: "Nederlands", englishDescription: "Dutch", t
user_profile: "Gebruikersprofiel"
patches: "Patches"
model: "Model"
# system: "System"
# component: "Component"
# components: "Components"
# thang: "Thang"
# thangs: "Thangs"
# level_session: "Your Session"
# opponent_session: "Opponent Session"
# article: "Article"
# user_names: "User Names"
# files: "Files"
# top_simulators: "Top Simulators"
system: "Systeem"
component: "Component"
components: "Componenten"
thang: "Thang"
thangs: "Thangs"
level_session: "Jouw Sessie"
opponent_session: "Sessie van tegenstander"
article: "Artikel"
user_names: "Gebruikersnamen"
files: "Bestanden"
top_simulators: "Top Simulatoren"

View file

@ -15,9 +15,9 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
fork: "派生"
play: "开始"
retry: "重试"
# watch: "Watch"
# unwatch: "Unwatch"
# submit_patch: "Submit Patch"
watch: "关注"
unwatch: "取消关注"
submit_patch: "提交补丁"
units:
second: ""
@ -166,8 +166,8 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
email_notifications: "通知"
# email_notifications_summary: "Controls for personalized, automatic email notifications related to your CodeCombat activity."
email_any_notes: "任何通知"
# email_any_notes_description: "Disable to stop all activity notification emails."
# email_recruit_notes: "Job Opportunities"
email_any_notes_description: "取消接收所有活动提醒邮件"
email_recruit_notes: "工作机会"
# email_recruit_notes_description: "If you play really well, we may contact you about getting you a (better) job."
contributor_emails: "贡献者通知"
contribute_prefix: "我们在寻找志同道合的人!请到"
@ -181,7 +181,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
job_profile_approved: "你填写的工作经历将由CodeCombat认证. 雇主将看到这些信息,除非你将它设置为不启用状态或者连续四周没有更新."
job_profile_explanation: "你好! 填写这些信息, 我们将使用它帮你寻找一份软件开发的工作."
# sample_profile: "See a sample profile"
# view_profile: "View Your Profile"
view_profile: "浏览个人信息"
account_profile:
edit_settings: "编辑设置"
@ -199,7 +199,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
employers:
want_to_hire_our_players: "想要雇用CodeCombat上的专业玩家"
# see_candidates: "Click here to see our candidates"
see_candidates: "点击这里查看我们的忧患人"
candidates_count_prefix: "我们当前有 "
candidates_count_many: "很多"
candidates_count_suffix: "经过我们认证的高手们正在找工作。"
@ -210,8 +210,8 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
candidate_top_skills: "高级技能"
candidate_years_experience: "多年工作经验"
candidate_last_updated: "最后一次更新"
# candidate_approved: "Us?"
# candidate_active: "Them?"
candidate_approved: "我们"
candidate_active: "他们"
play_level:
level_load_error: "关卡不能载入: "
@ -271,7 +271,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
editor_config_invisibles_description: "显示诸如空格或TAB键。"
editor_config_indentguides_label: "显示缩进提示"
editor_config_indentguides_description: "显示一条竖线以使缩进更明显。"
# editor_config_behaviors_label: "Smart Behaviors"
editor_config_behaviors_label: "聪明的行为"
editor_config_behaviors_description: "自动完成括号,大括号和引号。"
loading_ready: "载入完成!"
tip_insert_positions: "使用Shift+左键来插入拼写编辑器。"
@ -320,7 +320,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
lg_title: "最新的游戏"
# clas: "CLAs"
# community:
community:
level_editor: "关卡编辑器"
main_title: "CodeCombat 社区"
# facebook: "Facebook"
@ -336,7 +336,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
thang_description: "创建单元,并定义单元的逻辑、图形和音频。目前只支持导入 Flash 导出的矢量图形。"
level_title: "关卡编辑器"
level_description: "所有用来创造所有难度的关卡的工具,包括脚本、上传音频和构建自定义逻辑。"
# got_questions: "Questions about using the CodeCombat editors?"
got_questions: "使用CodeCombat编辑器有问题"
contact_us: "联系我们!"
hipchat_prefix: "你也可以在这里找到我们"
hipchat_url: "HipChat 房间。"
@ -378,9 +378,9 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
new_article_title: "创建一个新物品"
new_thang_title: "创建一个新物品类型"
new_level_title: "创建一个新关卡"
# new_article_title_signup: "Sign Up to Create a New Article"
# new_thang_title_signup: "Sign Up to Create a New Thang Type"
# new_level_title_signup: "Sign Up to Create a New Level"
new_article_title_signup: "注册以创建新的物品"
new_thang_title_signup: "注册以创建新的物品类型"
new_level_title_signup: "注册以创建新的关卡"
article_search_title: "在这里搜索物品"
thang_search_title: "在这里搜索物品类型"
level_search_title: "在这里搜索关卡"

View file

@ -24,3 +24,15 @@ module.exports = class LevelSession extends CocoModel
code = @get('code')
parts = spellKey.split '/'
code?[parts[0]]?[parts[1]]
readyToRank: ->
return false unless @get('levelID') # If it hasn't been denormalized, then it's not ready.
return false unless c1 = @get('code')
return false unless team = @get('team')
return true unless c2 = @get('submittedCode')
thangSpellArr = (s.split("/") for s in @get('teamSpells')[team])
for item in thangSpellArr
thang = item[0]
spell = item[1]
return true if c1[thang][spell] isnt c2[thang][spell]
false

View file

@ -4,6 +4,7 @@ ThangComponentSchema = require './thang_component'
SpecificArticleSchema = c.object()
c.extendNamedProperties SpecificArticleSchema # name first
SpecificArticleSchema.properties.body = { type: 'string', title: 'Content', description: "The body content of the article, in Markdown.", format: 'markdown' }
SpecificArticleSchema.properties.i18n = {type: "object", format: 'i18n', props: ['name', 'body'], description: "Help translate this article"}
SpecificArticleSchema.displayProperty = 'name'
side = {title: "Side", description: "A side.", type: 'string', 'enum': ['left', 'right', 'top', 'bottom']}

View file

@ -50,6 +50,9 @@
#settings-col
float: left
width: 550px
.treema-row img
max-width: 100%
#thang-type-treema
height: 400px
@ -66,6 +69,8 @@
background-color: white
border-radius: 4px
#spritesheets
border: 1px solid green
max-width: 100%

View file

@ -10,6 +10,8 @@
display: none
#docs-button
display: none
#gold-view
right: 1%
#control-bar-view
width: 100%
@ -41,7 +43,7 @@
margin: 0 auto
canvas#surface
background-color: #ddd
background-color: #333
max-height: 93%
max-height: -webkit-calc(100% - 60px)
max-height: calc(100% - 60px)

View file

@ -27,10 +27,11 @@ block modal-body-content
if ladderGame
if me.get('anonymous')
p Sign in or create an account and get your solution on the leaderboard!
p(data-i18n="play_level.multiplayer_sign_in_leaderboard") Sign in or create an account and get your solution on the leaderboard.
else if readyToRank
button.btn.btn-success.rank-game-button(data-i18n="play_level.victory_rank_my_game") Rank My Game
else
a#go-to-leaderboard-button.btn.btn-primary(href="/play/ladder/#{levelSlug}#my-matches") Go to the leaderboard!
p You can submit your game to be ranked from the leaderboard page.
a.btn.btn-primary(href="/play/ladder/#{levelSlug}#my-matches", data-i18n="play_level.victory_go_ladder") Return to Ladder
block modal-footer-content
a(href='#', data-dismiss="modal", aria-hidden="true", data-i18n="modal.close").btn.btn-primary Close

View file

@ -17,14 +17,11 @@ module.exports = class PatchModal extends ModalView
constructor: (@patch, @targetModel, options) ->
super(options)
targetID = @patch.get('target').id
if false
@originalSource = targetModel.clone(false)
@onOriginalLoaded()
if targetID is @targetModel.id
@originalSource = @targetModel.clone(false)
else
@originalSource = new targetModel.constructor({_id:targetID})
@originalSource.fetch()
@listenToOnce @originalSource, 'sync', @onOriginalLoaded
@addResourceToLoad(@originalSource)
@originalSource = new @targetModel.constructor({_id:targetID})
@supermodel.loadModel @originalSource, 'source_document'
getRenderData: ->
c = super()
@ -35,7 +32,7 @@ module.exports = class PatchModal extends ModalView
c
afterRender: ->
return if @originalSource.loading
return unless @supermodel.finished()
headModel = null
if @targetModel.hasWriteAccess()
headModel = @originalSource.clone(false)

View file

@ -45,7 +45,7 @@ module.exports = class LadderTabView extends CocoView
checkFriends: ->
return if @checked or (not window.FB) or (not window.gapi)
@checked = true
# @addSomethingToLoad("facebook_status")
@fbStatusRes = @supermodel.addSomethingResource("facebook_status", 0)
@ -78,7 +78,7 @@ module.exports = class LadderTabView extends CocoView
@fbFriendRes.load()
FB.api '/me/friends', @onFacebookFriendsLoaded
onFacebookFriendsLoaded: (response) =>
@facebookData = response.data
@loadFacebookFriendSessions()
@ -104,7 +104,7 @@ module.exports = class LadderTabView extends CocoView
friend.otherTeam = if friend.team is 'humans' then 'ogres' else 'humans'
friend.imageSource = "http://graph.facebook.com/#{friend.facebookID}/picture"
@facebookFriendSessions = result
# GOOGLE PLUS
onConnectGPlus: ->
@ -113,7 +113,7 @@ module.exports = class LadderTabView extends CocoView
application.gplusHandler.reauthorize()
onConnectedWithGPlus: -> location.reload() if @connecting
gplusSessionStateLoaded: ->
if application.gplusHandler.loggedIn
#@addSomethingToLoad("gplus_friends")
@ -146,7 +146,7 @@ module.exports = class LadderTabView extends CocoView
friend.otherTeam = if friend.team is 'humans' then 'ogres' else 'humans'
friend.imageSource = friendsMap[friend.gplusID].image.url
@gplusFriendSessions = result
# LADDER LOADING
refreshLadder: ->
@ -160,7 +160,7 @@ module.exports = class LadderTabView extends CocoView
render: ->
super()
@$el.find('.histogram-display').each (i, el) =>
histogramWrapper = $(el)
team = _.find @teams, name: histogramWrapper.data('team-name')
@ -168,8 +168,8 @@ module.exports = class LadderTabView extends CocoView
$.when(
$.get("/db/level/#{@level.get('slug')}/histogram_data?team=#{team.name.toLowerCase()}", (data) -> histogramData = data)
).then =>
@generateHistogram(histogramWrapper, histogramData, team.name.toLowerCase())
@generateHistogram(histogramWrapper, histogramData, team.name.toLowerCase()) unless @destroyed
getRenderData: ->
ctx = super()
ctx.level = @level
@ -186,7 +186,7 @@ module.exports = class LadderTabView extends CocoView
#renders twice, hack fix
if $("#"+histogramElement.attr("id")).has("svg").length then return
histogramData = histogramData.map (d) -> d*100
margin =
top: 20
right: 20
@ -195,17 +195,17 @@ module.exports = class LadderTabView extends CocoView
width = 300 - margin.left - margin.right
height = 125 - margin.top - margin.bottom
formatCount = d3.format(",.0")
x = d3.scale.linear().domain([-3000,6000]).range([0,width])
data = d3.layout.histogram().bins(x.ticks(20))(histogramData)
y = d3.scale.linear().domain([0,d3.max(data, (d) -> d.y)]).range([height,0])
#create the x axis
xAxis = d3.svg.axis().scale(x).orient("bottom").ticks(5).outerTickSize(0)
svg = d3.select("#"+histogramElement.attr("id")).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
@ -214,13 +214,13 @@ module.exports = class LadderTabView extends CocoView
barClass = "bar"
if teamName.toLowerCase() is "ogres" then barClass = "ogres-bar"
if teamName.toLowerCase() is "humans" then barClass = "humans-bar"
bar = svg.selectAll(".bar")
.data(data)
.enter().append("g")
.attr("class",barClass)
.attr("transform", (d) -> "translate(#{x(d.x)},#{y(d.y)})")
.attr("transform", (d) -> "translate(#{x(d.x)},#{y(d.y)})")
bar.append("rect")
.attr("x",1)
.attr("width",width/20)
@ -232,7 +232,7 @@ module.exports = class LadderTabView extends CocoView
.enter().append("g")
.attr("class","specialbar")
.attr("transform", "translate(#{x(playerScore)},#{y(9001)})")
scorebar.append("rect")
.attr("x",1)
.attr("width",3)
@ -240,9 +240,9 @@ module.exports = class LadderTabView extends CocoView
rankClass = "rank-text"
if teamName.toLowerCase() is "ogres" then rankClass = "rank-text ogres-rank-text"
if teamName.toLowerCase() is "humans" then rankClass = "rank-text humans-rank-text"
message = "#{histogramData.length} players"
if @leaderboards[teamName].session? then message="#{@leaderboards[teamName].myRank}/#{histogramData.length}"
if @leaderboards[teamName].session? then message="##{@leaderboards[teamName].myRank} of #{histogramData.length}"
svg.append("g")
.append("text")
.attr("class",rankClass)
@ -250,14 +250,14 @@ module.exports = class LadderTabView extends CocoView
.attr("text-anchor","end")
.attr("x",width)
.text(message)
#Translate the x-axis up
svg.append("g")
.attr("class", "x axis")
.attr("transform","translate(0," + height + ")")
.call(xAxis)
consolidateFriends: ->
allFriendSessions = (@facebookFriendSessions or []).concat(@gplusFriendSessions or [])
sessions = _.uniq allFriendSessions, false, (session) -> session._id
@ -312,7 +312,7 @@ class LeaderboardData extends CocoClass
@trigger 'sync', @
# TODO: cache user ids -> names mapping, and load them here as needed,
# and apply them to sessions. Fetching each and every time is too costly.
onFail: (resource, jqxhr) =>
return if @destroyed
@trigger 'error', @, jqxhr

View file

@ -72,7 +72,7 @@ module.exports = class MyMatchesTabView extends CocoView
for team in @teams
team.session = (s for s in @sessions.models when s.get('team') is team.id)[0]
team.readyToRank = @readyToRank(team.session)
team.readyToRank = team.session?.readyToRank()
team.isRanking = team.session?.get('isRanking')
team.matches = (convertMatch(match, team.session.get('submitDate')) for match in team.session?.get('matches') or [])
team.matches.reverse()
@ -84,7 +84,7 @@ module.exports = class MyMatchesTabView extends CocoView
if scoreHistory?.length > 1
team.scoreHistory = scoreHistory
scoreHistory = _.last scoreHistory, 100 # Chart URL needs to be under 2048 characters for GET
team.currentScore = Math.round scoreHistory[scoreHistory.length - 1][1] * 100
team.chartColor = team.primaryColor.replace '#', ''
#times = (s[0] for s in scoreHistory)
@ -108,36 +108,35 @@ module.exports = class MyMatchesTabView extends CocoView
sessionID = button.data('session-id')
session = _.find @sessions.models, {id: sessionID}
rankingState = 'unavailable'
if @readyToRank session
if session.readyToRank()
rankingState = 'rank'
else if session.get 'isRanking'
rankingState = 'ranking'
@setRankingButtonText button, rankingState
@$el.find('.score-chart-wrapper').each (i, el) =>
scoreWrapper = $(el)
team = _.find @teams, name: scoreWrapper.data('team-name')
@generateScoreLineChart(scoreWrapper.attr('id'), team.scoreHistory, team.name)
generateScoreLineChart: (wrapperID, scoreHistory,teamName) =>
margin =
margin =
top: 20
right: 20
bottom: 30
left: 50
width = 450 - margin.left - margin.right
height = 125
x = d3.time.scale().range([0,width])
y = d3.scale.linear().range([height,0])
xAxis = d3.svg.axis().scale(x).orient("bottom").ticks(4).outerTickSize(0)
yAxis = d3.svg.axis().scale(y).orient("left").ticks(4).outerTickSize(0)
line = d3.svg.line().x(((d) -> x(d.date))).y((d) -> y(d.close))
selector = "#" + wrapperID
svg = d3.select(selector).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
@ -150,12 +149,10 @@ module.exports = class MyMatchesTabView extends CocoView
date: time
close: d[1] * 100
}
x.domain(d3.extent(data, (d) -> d.date))
y.domain(d3.extent(data, (d) -> d.close))
svg.append("g")
.attr("class", "y axis")
.call(yAxis)
@ -172,21 +169,6 @@ module.exports = class MyMatchesTabView extends CocoView
.datum(data)
.attr("class",lineClass)
.attr("d",line)
readyToRank: (session) ->
return false unless session?.get('levelID') # If it hasn't been denormalized, then it's not ready.
return false unless c1 = session.get('code')
return false unless team = session.get('team')
return true unless c2 = session.get('submittedCode')
thangSpellArr = (s.split("/") for s in session.get('teamSpells')[team])
for item in thangSpellArr
thang = item[0]
spell = item[1]
return true if c1[thang][spell] isnt c2[thang][spell]
return false
rankSession: (e) ->
button = $(e.target).closest('.rank-button')
@ -202,7 +184,6 @@ module.exports = class MyMatchesTabView extends CocoView
@setRankingButtonText(button, 'failed')
ajaxData = {session: sessionID, levelID: @level.id, originalLevelID: @level.attributes.original, levelMajorVersion: @level.attributes.version.major}
console.log "Posting game for ranking from My Matches view."
$.ajax '/queue/scoring', {
type: 'POST'
data: ajaxData

View file

@ -35,7 +35,6 @@ module.exports = class ControlBarView extends View
@session = options.session
@level = options.level
@playableTeams = options.playableTeams
@ladderGame = options.ladderGame
@spectateGame = options.spectateGame ? false
super options
@ -55,13 +54,12 @@ module.exports = class ControlBarView extends View
super c
c.worldName = @worldName
c.multiplayerEnabled = @session.get('multiplayer')
c.ladderGame = @ladderGame
c.ladderGame = @level.get('type') is 'ladder'
c.spectateGame = @spectateGame
c.homeLink = "/"
levelID = @level.get('slug')
if levelID in ["brawlwood", "brawlwood-tutorial", "dungeon-arena", "dungeon-arena-tutorial"]
levelID = 'brawlwood' if levelID is 'brawlwood-tutorial'
c.homeLink = "/play/ladder/" + levelID
if @level.get('type') in ['ladder', 'ladder-tutorial']
c.homeLink = '/play/ladder/' + @level.get('slug').replace /\-tutorial$/, ''
else
c.homeLink = '/'
c
afterRender: ->

View file

@ -9,7 +9,7 @@ module.exports = class MultiplayerModal extends View
events:
'click textarea': 'onClickLink'
'change #multiplayer': 'updateLinkSection'
'click .rank-game-button': 'onRankGame'
constructor: (options) ->
super(options)
@ -17,20 +17,20 @@ module.exports = class MultiplayerModal extends View
@level = options.level
@listenTo(@session, 'change:multiplayer', @updateLinkSection)
@playableTeams = options.playableTeams
@ladderGame = options.ladderGame
console.log 'ladder game is', @ladderGame
getRenderData: ->
c = super()
c.joinLink = (document.location.href.replace(/\?.*/, '').replace('#', '') +
'?session=' +
@session.id)
c.multiplayer = @session.get('multiplayer')
c.multiplayer = @session.get 'multiplayer'
c.team = @session.get 'team'
c.levelSlug = @level?.get('slug')
c.levelSlug = @level?.get 'slug'
c.playableTeams = @playableTeams
c.ladderGame = @ladderGame
# For now, ladderGame will disallow multiplayer, because session code combining doesn't play nice yet.
if @level?.get('type') is 'ladder'
c.ladderGame = true
c.readyToRank = @session?.readyToRank()
c
afterRender: ->
@ -50,5 +50,20 @@ module.exports = class MultiplayerModal extends View
multiplayer = Boolean(@$el.find('#multiplayer').prop('checked'))
@session.set('multiplayer', multiplayer)
onRankGame: (e) ->
button = @$el.find('.rank-game-button')
button.text($.i18n.t('play_level.victory_ranking_game', defaultValue: 'Submitting...'))
button.prop 'disabled', true
ajaxData = session: @session.id, levelID: @level.id, originalLevelID: @level.get('original'), levelMajorVersion: @level.get('version').major
ladderURL = "/play/ladder/#{@level.get('slug')}#my-matches"
goToLadder = -> Backbone.Mediator.publish 'router:navigate', route: ladderURL
$.ajax '/queue/scoring',
type: 'POST'
data: ajaxData
success: goToLadder
failure: (response) ->
console.error "Couldn't submit game for ranking:", response
goToLadder()
destroy: ->
super()

View file

@ -65,7 +65,6 @@ module.exports = class VictoryModal extends View
ajaxData = session: @session.id, levelID: @level.id, originalLevelID: @level.get('original'), levelMajorVersion: @level.get('version').major
ladderURL = "/play/ladder/#{@level.get('slug')}#my-matches"
goToLadder = -> Backbone.Mediator.publish 'router:navigate', route: ladderURL
console.log "Posting game for ranking from victory modal."
$.ajax '/queue/scoring',
type: 'POST'
data: ajaxData
@ -82,9 +81,7 @@ module.exports = class VictoryModal extends View
c.levelName = utils.i18n @level.attributes, 'name'
c.level = @level
if c.level.get('type') is 'ladder'
c1 = @session?.get('code')
c2 = @session?.get('submittedCode')
c.readyToRank = @session.get('levelID') and c1 and not _.isEqual(c1, c2)
c.readyToRank = @session.readyToRank()
if me.get 'hourOfCode'
# Show the Hour of Code "I'm Done" tracking pixel after they played for 30 minutes
elapsed = (new Date() - new Date(me.get('dateCreated')))

View file

@ -97,8 +97,12 @@ module.exports = class SpellView extends View
aceCommands.push c.name
addCommand
name: 'run-code'
bindKey: {win: 'Shift-Enter|Ctrl-Enter|Ctrl-S', mac: 'Shift-Enter|Command-Enter|Ctrl-Enter|Command-S|Ctrl-S'}
bindKey: {win: 'Shift-Enter|Ctrl-Enter', mac: 'Shift-Enter|Command-Enter|Ctrl-Enter'}
exec: -> Backbone.Mediator.publish 'tome:manual-cast', {}
addCommand
name: 'no-op'
bindKey: {win: 'Ctrl-S', mac: 'Command-S|Ctrl-S'}
exec: -> # just prevent page save call
addCommand
name: 'toggle-playing'
bindKey: {win: 'Ctrl-P', mac: 'Command-P|Ctrl-P'}

View file

@ -174,7 +174,7 @@ module.exports = class PlayLevelView extends View
@initSurface()
@initGoalManager()
@initScriptManager()
@insertSubviews ladderGame: (@level.get('type') is "ladder")
@insertSubviews()
@initVolume()
@listenTo(@session, 'change:multiplayer', @onMultiplayerChanged)
@originalSessionState = $.extend(true, {}, @session.get('state'))
@ -238,15 +238,15 @@ module.exports = class PlayLevelView extends View
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.fillText("Loaded #{@modelsLoaded} thingies",50,50)
insertSubviews: (subviewOptions) ->
@insertSubView @tome = new TomeView levelID: @levelID, session: @session, thangs: @world.thangs, supermodel: @supermodel, ladderGame: subviewOptions.ladderGame
insertSubviews: ->
@insertSubView @tome = new TomeView levelID: @levelID, session: @session, thangs: @world.thangs, supermodel: @supermodel
@insertSubView new PlaybackView {}
@insertSubView new GoalsView {}
@insertSubView new GoldView {}
@insertSubView new HUDView {}
@insertSubView new ChatView levelID: @levelID, sessionID: @session.id, session: @session
worldName = utils.i18n @level.attributes, 'name'
@controlBar = @insertSubView new ControlBarView {worldName: worldName, session: @session, level: @level, supermodel: @supermodel, playableTeams: @world.playableTeams, ladderGame: subviewOptions.ladderGame}
@controlBar = @insertSubView new ControlBarView {worldName: worldName, session: @session, level: @level, supermodel: @supermodel, playableTeams: @world.playableTeams}
#Backbone.Mediator.publish('level-set-debug', debug: true) if me.displayName() is 'Nick!'
afterInsert: ->

View file

@ -161,7 +161,7 @@ module.exports = class SpectateLevelView extends View
@initSurface()
@initGoalManager()
@initScriptManager()
@insertSubviews ladderGame: @otherSession?
@insertSubviews()
@initVolume()
@originalSessionState = $.extend(true, {}, @session.get('state'))
@ -229,8 +229,8 @@ module.exports = class SpectateLevelView extends View
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.fillText("Loaded #{@modelsLoaded} thingies",50,50)
insertSubviews: (subviewOptions) ->
@insertSubView @tome = new TomeView levelID: @levelID, session: @session, thangs: @world.thangs, supermodel: @supermodel, ladderGame: subviewOptions.ladderGame
insertSubviews: ->
@insertSubView @tome = new TomeView levelID: @levelID, session: @session, thangs: @world.thangs, supermodel: @supermodel
@insertSubView new PlaybackView {}
@insertSubView new GoldView {}

View file

@ -8,5 +8,5 @@ current_directory = os.path.dirname(os.path.realpath(sys.argv[0]))
coco_path = os.getenv("COCO_DIR",os.path.join(current_directory,os.pardir))
nodemon_path = coco_path + os.sep + "node_modules" + os.sep + ".bin" + os.sep + "nodemon"
call(nodemon_path + " . --ext \".coffee|.js\" --watch server --watch app.js --watch server_config.js --watch server_setup.coffee",shell=True,cwd=coco_path)
call(nodemon_path + " . --ext \".coffee|.js\" --watch server --watch server_config.js --watch server_setup.coffee --watch app" + os.sep + "schemas",shell=True,cwd=coco_path)

View file

@ -46,7 +46,7 @@
"mongoose": "3.8.x",
"mongoose-text-search": "~0.0.2",
"request": "2.12.x",
"tv4": "1.0.x",
"tv4": "~1.0.16",
"lodash": "~2.0.0",
"underscore.string": "2.3.x",
"async": "0.2.x",
@ -92,7 +92,8 @@
"karma-phantomjs-launcher": "~0.1.1",
"karma": "~0.10.9",
"karma-coverage": "~0.1.4",
"compressible": "~1.0.1"
"compressible": "~1.0.1",
"jasmine-spec-reporter":"~0.3.0"
},
"license": "MIT for the code, and CC-BY for the art and music",
"private": true,

View file

@ -15,7 +15,7 @@ module.exports.connect = () ->
module.exports.generateMongoConnectionString = ->
if config.mongo.mongoose_replica_string
if not testing and config.mongo.mongoose_replica_string
address = config.mongo.mongoose_replica_string
else
dbName = config.mongo.db
@ -25,4 +25,4 @@ module.exports.generateMongoConnectionString = ->
address = config.mongo.username + ":" + config.mongo.password + "@" + address
address = "mongodb://#{address}/#{dbName}"
return address
return address

View file

@ -76,18 +76,18 @@ handleLadderUpdate = (req, res) ->
sendLadderUpdateEmail result, now, daysAgo for result in results
sendLadderUpdateEmail = (session, now, daysAgo) ->
User.findOne({_id: session.creator}).select("name email firstName lastName emailSubscriptions emails preferredLanguage").lean().exec (err, user) ->
User.findOne({_id: session.creator}).select("name email firstName lastName emailSubscriptions emails preferredLanguage").exec (err, user) ->
if err
log.error "Couldn't find user for #{session.creator} from session #{session._id}"
return
allowNotes = user.isEmailSubscriptionEnabled 'anyNotes'
unless user.email and allowNotes and not session.unsubscribed
log.info "Not sending email to #{user.email} #{user.name} because they only want emails about #{user.emailSubscriptions}, #{user.emails} - session unsubscribed: #{session.unsubscribed}"
unless user.get('email') and allowNotes and not session.unsubscribed
log.info "Not sending email to #{user.get('email')} #{user.get('name')} because they only want emails about #{user.get('emailSubscriptions')}, #{user.get('emails')} - session unsubscribed: #{session.unsubscribed}"
return
unless session.levelName
log.info "Not sending email to #{user.email} #{user.name} because the session had no levelName in it."
log.info "Not sending email to #{user.get('email')} #{user.get('name')} because the session had no levelName in it."
return
name = if user.firstName and user.lastName then "#{user.firstName}" else user.name
name = if user.get('firstName') and user.get('lastName') then "#{user.get('firstName')}" else user.get('name')
name = "Wizard" if not name or name is "Anoner"
# Fetch the most recent defeat and victory, if there are any.
@ -108,7 +108,7 @@ sendLadderUpdateEmail = (session, now, daysAgo) ->
context =
email_id: sendwithus.templates.ladder_update_email
recipient:
address: if DEBUGGING then 'nick@codecombat.com' else user.email
address: if DEBUGGING then 'nick@codecombat.com' else user.get('email')
name: name
email_data:
name: name

View file

@ -105,11 +105,12 @@ UserHandler = class UserHandler extends Handler
(req, user, callback) ->
return callback(null, req, user) unless req.body.name
nameLower = req.body.name?.toLowerCase()
return callback(null, req, user) if nameLower is user.get('nameLower')
User.findOne({nameLower:nameLower}).exec (err, otherUser) ->
# return callback(null, req, user) if nameLower is user.get('nameLower')
User.findOne({nameLower:nameLower,anonymous:false}).exec (err, otherUser) ->
log.error "Database error setting user name: #{err}" if err
return callback(res:'Database error.', code:500) if err
r = {message:'is already used by another account', property:'name'}
console.log 'Another user exists' if otherUser
return callback({res:r, code:409}) if otherUser
user.set('name', req.body.name)
callback(null, req, user)
@ -127,7 +128,7 @@ UserHandler = class UserHandler extends Handler
@getPropertiesFromMultipleDocuments res, User, properties, ids
nameToID: (req, res, name) ->
User.findOne({nameLower:name.toLowerCase()}).exec (err, otherUser) ->
User.findOne({nameLower:name.toLowerCase(),anonymous:false}).exec (err, otherUser) ->
res.send(if otherUser then otherUser._id else JSON.stringify(''))
res.end()

View file

@ -3,7 +3,12 @@
console.log 'IT BEGINS'
require('jasmine-spec-reporter')
jasmine.getEnv().reporter.subReporters_ = []
jasmine.getEnv().addReporter(new jasmine.SpecReporter({
displaySuccessfulSpec: true,
displayFailedSpec: true
}))
GLOBAL._ = require('lodash')
_.str = require('underscore.string')
_.mixin(_.str.exports())

View file

@ -44,6 +44,20 @@ describe 'User.updateMailChimp', ->
describe 'POST /db/user', ->
createAnonNameUser = (done)->
request.post getURL('/auth/logout'), ->
request.get getURL('/auth/whoami'), ->
req = request.post(getURL('/db/user'), (err, response) ->
expect(response.statusCode).toBe(200)
request.get getURL('/auth/whoami'), (request, response, body) ->
res = JSON.parse(response.body)
expect(res.anonymous).toBeTruthy()
expect(res.name).toEqual('Jim')
done()
)
form = req.form()
form.append('name', 'Jim')
it 'preparing test : clears the db first', (done) ->
clearModels [User], (err) ->
throw err if err
@ -90,6 +104,36 @@ describe 'POST /db/user', ->
expect(user.passwordHash).toBeUndefined()
done()
it 'should allow setting anonymous user name', (done) ->
createAnonNameUser(done)
it 'should allow multiple anonymous users with same name', (done) ->
createAnonNameUser(done)
it 'should not allow setting existing user name to anonymous user', (done) ->
createAnonUser = ->
request.post getURL('/auth/logout'), ->
request.get getURL('/auth/whoami'), ->
req = request.post(getURL('/db/user'), (err, response) ->
expect(response.statusCode).toBe(409)
done()
)
form = req.form()
form.append('name', 'Jim')
req = request.post(getURL('/db/user'), (err,response,body) ->
expect(response.statusCode).toBe(200)
request.get getURL('/auth/whoami'), (request, response, body) ->
res = JSON.parse(response.body)
expect(res.anonymous).toBeFalsy()
createAnonUser()
)
form = req.form()
form.append('email', 'new@user.com')
form.append('password', 'new')
describe 'PUT /db/user', ->