diff --git a/achievement_fixtures.js b/achievement_fixtures.js deleted file mode 100644 index 56b6fb42f..000000000 --- a/achievement_fixtures.js +++ /dev/null @@ -1,12 +0,0 @@ -// Fixtures - -db.achievements.insert({ - query: '{"level.original": "52d97ecd32362bc86e004e87"}', - index: true, - slug: 'dungeon-arena-started', - name: 'Dungeon Arena started', - worth: 1, - collection: 'level.session', - description: 'Started playing Dungeon Arena.', - userField: 'creator' -}); diff --git a/app/Router.coffee b/app/Router.coffee index ca6af592b..a7d4eeb49 100644 --- a/app/Router.coffee +++ b/app/Router.coffee @@ -18,9 +18,10 @@ module.exports = class CocoRouter extends Backbone.Router 'about': go('AboutView') - 'account/profile(/:userID)': go('account/JobProfileView') + 'account': go('account/MainAccountView') 'account/settings': go('account/AccountSettingsView') 'account/unsubscribe': go('account/UnsubscribeView') + #'account/payment' 'admin': go('admin/MainAdminView') 'admin/candidates': go('admin/CandidatesView') @@ -50,7 +51,7 @@ module.exports = class CocoRouter extends Backbone.Router 'editor': go('editor/MainEditorView') 'editor/achievement': go('editor/achievement/AchievementSearchView') - 'editor/achievement': go('editor/achievement/AchievementEditView') + 'editor/achievement/:articleID': go('editor/achievement/AchievementEditView') 'editor/article': go('editor/article/ArticleSearchView') 'editor/article/preview': go('editor/article/ArticlePreviewView') 'editor/article/:articleID': go('editor/article/ArticleEditView') @@ -79,6 +80,11 @@ module.exports = class CocoRouter extends Backbone.Router 'test(/*subpath)': go('TestView') + 'user/:slugOrID': go('user/MainUserView') + 'user/:slugOrID/stats': go('user/AchievementsView') + 'user/:slugOrID/profile': go('user/JobProfileView') + #'user/:slugOrID/code': go('user/CodeView') + '*name': 'showNotFoundView' routeToServer: (e) -> diff --git a/app/application.coffee b/app/application.coffee index 4eb829c88..1b23abcbb 100644 --- a/app/application.coffee +++ b/app/application.coffee @@ -5,7 +5,6 @@ locale = require 'locale/locale' {me} = require 'lib/auth' Tracker = require 'lib/Tracker' CocoView = require 'views/kinds/CocoView' -AchievementNotify = require '../../templates/achievement_notify' marked.setOptions {gfm: true, sanitize: true, smartLists: true, breaks: false} @@ -40,7 +39,6 @@ Application = initialize: -> @facebookHandler = new FacebookHandler() @gplusHandler = new GPlusHandler() $(document).bind 'keydown', preventBackspace - $.notify.addStyle 'achievement', html: $(AchievementNotify()) @linkedinHandler = new LinkedInHandler() preload(COMMON_FILES) $.i18n.init { diff --git a/app/collections/AchievementCollection.coffee b/app/collections/AchievementCollection.coffee new file mode 100644 index 000000000..d3bbe0343 --- /dev/null +++ b/app/collections/AchievementCollection.coffee @@ -0,0 +1,6 @@ +CocoCollection = require 'collections/CocoCollection' +Achievement = require 'models/Achievement' + +module.exports = class AchievementCollection extends CocoCollection + url: '/db/achievement' + model: Achievement diff --git a/app/collections/EarnedAchievementCollection.coffee b/app/collections/EarnedAchievementCollection.coffee new file mode 100644 index 000000000..82207afeb --- /dev/null +++ b/app/collections/EarnedAchievementCollection.coffee @@ -0,0 +1,9 @@ +CocoCollection = require 'collections/CocoCollection' +EarnedAchievement = require 'models/EarnedAchievement' + +module.exports = class EarnedAchievementCollection extends CocoCollection + model: EarnedAchievement + + initialize: (userID) -> + @url = "/db/user/#{userID}/achievements" + super() diff --git a/app/collections/NewAchievementCollection.coffee b/app/collections/NewAchievementCollection.coffee index 7b33d7d75..b9e4326b5 100644 --- a/app/collections/NewAchievementCollection.coffee +++ b/app/collections/NewAchievementCollection.coffee @@ -1,6 +1,9 @@ CocoCollection = require 'collections/CocoCollection' +Achievement = require 'models/Achievement' class NewAchievementCollection extends CocoCollection + model: Achievement + initialize: (me = require('lib/auth').me) -> @url = "/db/user/#{me.id}/achievements?notified=false" diff --git a/app/collections/RecentlyPlayedCollection.coffee b/app/collections/RecentlyPlayedCollection.coffee new file mode 100644 index 000000000..ff76aaf5b --- /dev/null +++ b/app/collections/RecentlyPlayedCollection.coffee @@ -0,0 +1,9 @@ +CocoCollection = require './CocoCollection' +LevelSession = require 'models/LevelSession' + +module.exports = class RecentlyPlayedCollection extends CocoCollection + model: LevelSession + + constructor: (userID, options) -> + @url = "/db/user/#{userID}/recently_played" + super options diff --git a/app/collections/RelatedAchievementsCollection.coffee b/app/collections/RelatedAchievementsCollection.coffee new file mode 100644 index 000000000..e558d2891 --- /dev/null +++ b/app/collections/RelatedAchievementsCollection.coffee @@ -0,0 +1,10 @@ +CocoCollection = require 'collections/CocoCollection' +Achievement = require 'models/Achievement' + +class RelatedAchievementCollection extends CocoCollection + model: Achievement + + initialize: (relatedID) -> + @url = "/db/achievement?related=#{relatedID}" + +module.exports = RelatedAchievementCollection diff --git a/app/lib/LevelBus.coffee b/app/lib/LevelBus.coffee index 845020c8d..7ea0837c1 100644 --- a/app/lib/LevelBus.coffee +++ b/app/lib/LevelBus.coffee @@ -1,6 +1,7 @@ Bus = require './Bus' {me} = require 'lib/auth' LevelSession = require 'models/LevelSession' +utils = require 'lib/utils' module.exports = class LevelBus extends Bus @@ -22,6 +23,7 @@ module.exports = class LevelBus extends Bus 'tome:spell-changed': 'onSpellChanged' 'tome:spell-created': 'onSpellCreated' 'application:idle-changed': 'onIdleChanged' + 'goal-manager:new-goal-states': 'onNewGoalStates' constructor: -> super(arguments...) @@ -192,6 +194,14 @@ module.exports = class LevelBus extends Bus @changedSessionProperties.state = true @saveSession() + onNewGoalStates: ({goalStates})-> + state = @session.get 'state' + unless utils.kindaEqual state.goalStates, goalStates # Only save when goals really change + state.goalStates = goalStates + @session.set 'state', state + @changedSessionProperties.state = true + @saveSession() + onPlayerJoined: (snapshot) => super(arguments...) return unless @onPoint() diff --git a/app/lib/LocalMongo.coffee b/app/lib/LocalMongo.coffee index 2027e1c70..558a58e5d 100644 --- a/app/lib/LocalMongo.coffee +++ b/app/lib/LocalMongo.coffee @@ -24,6 +24,7 @@ doQuerySelector = (value, operatorObj) -> matchesQuery = (target, queryObj) -> return true unless queryObj + throw new Error 'Expected an object to match a query against, instead got null' unless target for prop, query of queryObj if prop[0] == '$' switch prop diff --git a/app/lib/SystemNameLoader.coffee b/app/lib/SystemNameLoader.coffee index c25abb914..c4793d735 100644 --- a/app/lib/SystemNameLoader.coffee +++ b/app/lib/SystemNameLoader.coffee @@ -1,4 +1,4 @@ -CocoClass = require 'lib/CocoClass' +CocoClass = require './CocoClass' namesCache = {} diff --git a/app/lib/deltas.coffee b/app/lib/deltas.coffee index 99df202aa..55743f345 100644 --- a/app/lib/deltas.coffee +++ b/app/lib/deltas.coffee @@ -1,4 +1,4 @@ -SystemNameLoader = require 'lib/SystemNameLoader' +SystemNameLoader = require './SystemNameLoader' ### Good-to-knows: dataPath: an array of keys that walks you up a JSON object that's being patched @@ -12,7 +12,7 @@ module.exports.expandDelta = (delta, left, schema) -> (expandFlattenedDelta(fd, left, schema) for fd in flattenedDeltas) -flattenDelta = (delta, dataPath=null, deltaPath=null) -> +module.exports.flattenDelta = flattenDelta = (delta, dataPath=null, deltaPath=null) -> # takes a single jsondiffpatch delta and returns an array of objects with return [] unless delta dataPath ?= [] @@ -175,3 +175,5 @@ prunePath = (delta, path) -> prunePath delta[path[0]], path.slice(1) unless delta[path[0]] is undefined keys = (k for k in _.keys(delta[path[0]]) when k isnt '_t') delete delta[path[0]] if keys.length is 0 + + diff --git a/app/lib/utils.coffee b/app/lib/utils.coffee index ae556a431..c009af43a 100644 --- a/app/lib/utils.coffee +++ b/app/lib/utils.coffee @@ -70,6 +70,7 @@ module.exports.i18n = (say, target, language=me.lang(), fallback='en') -> null module.exports.getByPath = (target, path) -> + throw new Error 'Expected an object to match a query against, instead got null' unless target pieces = path.split('.') obj = target for piece in pieces @@ -82,7 +83,7 @@ module.exports.isID = (id) -> _.isString(id) and id.length is 24 and id.match(/[ module.exports.round = _.curry (digits, n) -> n = +n.toFixed(digits) -positify = (func) -> (x) -> if x > 0 then func(x) else 0 +positify = (func) -> (params) -> (x) -> if x > 0 then func(params)(x) else 0 # f(x) = ax + b createLinearFunc = (params) -> @@ -100,3 +101,32 @@ module.exports.functionCreators = linear: positify(createLinearFunc) quadratic: positify(createQuadraticFunc) logarithmic: positify(createLogFunc) + +# Call done with true to satisfy the 'until' goal and stop repeating func +module.exports.keepDoingUntil = (func, wait=100, totalWait=5000) -> + waitSoFar = 0 + (done = (success) -> + if (waitSoFar += wait) <= totalWait and not success + _.delay (-> func done), wait) false + +module.exports.grayscale = (imageData) -> + d = imageData.data + for i in [0..d.length] by 4 + r = d[i] + g = d[i+1] + b = d[i+2] + v = 0.2126*r + 0.7152*g + 0.0722*b + d[i] = d[i+1] = d[i+2] = v + imageData + +# Deep compares l with r, with the exception that undefined values are considered equal to missing values +# Very practical for comparing Mongoose documents where undefined is not allowed, instead fields get deleted +module.exports.kindaEqual = compare = (l, r) -> + if _.isObject(l) and _.isObject(r) + for key in _.union Object.keys(l), Object.keys(r) + return false unless compare l[key], r[key] + return true + else if l is r + return true + else + return false diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 0bbbbcabb..260f5f8d3 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -49,6 +49,9 @@ blog: "Blog" forum: "Forum" account: "Account" + profile: "Profile" + stats: "Stats" + code: "Code" admin: "Admin" home: "Home" contribute: "Contribute" @@ -176,12 +179,14 @@ new_password: "New Password" new_password_verify: "Verify" email_subscriptions: "Email Subscriptions" + email_subscriptions_none: "No Email Subscriptions." email_announcements: "Announcements" email_announcements_description: "Get emails on the latest news and developments at CodeCombat." email_notifications: "Notifications" 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_news: "News" email_recruit_notes: "Job Opportunities" email_recruit_notes_description: "If you play really well, we may contact you about getting you a (better) job." contributor_emails: "Contributor Class Emails" @@ -591,6 +596,10 @@ level_search_title: "Search Levels Here" achievement_search_title: "Search Achievements" read_only_warning2: "Note: you can't save any edits here, because you're not logged in." + no_achievements: "No achievements have been added for this level yet." + achievement_query_misc: "Key achievement off of miscellanea" + achievement_query_goals: "Key achievement off of level goals" + level_completion: "Level Completion" article: edit_btn_preview: "Preview" @@ -599,6 +608,7 @@ general: and: "and" name: "Name" + date: "Date" body: "Body" version: "Version" commit_msg: "Commit Message" @@ -938,3 +948,38 @@ text_diff: "Text Diff" merge_conflict_with: "MERGE CONFLICT WITH" no_changes: "No Changes" + + user: + stats: "Stats" + singleplayer_title: "Singleplayer Levels" + multiplayer_title: "Multiplayer Levels" + achievements_title: "Achievements" + last_played: "Last Played" + status: "Status" + status_completed: "Completed" + status_unfinished: "Unfinished" + no_singleplayer: "No Singleplayer games played yet." + no_multiplayer: "No Multiplayer games played yet." + no_achievements: "No Achievements earned yet." + favorite_prefix: "Favorite language is " + favorite_postfix: "." + + achievements: + last_earned: "Last Earned" + amount_achieved: "Amount" + achievement: "Achievement" + category_contributor: "Contributor" + category_miscellaneous: "Miscellaneous" + category_levels: "Levels" + category_undefined: "Uncategorized" + current_xp_prefix: "" + current_xp_postfix: " in total" + new_xp_prefix: "" + new_xp_postfix: " earned" + left_xp_prefix: "" + left_xp_infix: " until level " + left_xp_postfix: "" + + account: + recently_played: "Recently Played" + no_recent_games: "No games played during the past two weeks." diff --git a/app/locale/locale.coffee b/app/locale/locale.coffee index 1fcf2e202..5b970d28f 100644 --- a/app/locale/locale.coffee +++ b/app/locale/locale.coffee @@ -26,7 +26,7 @@ module.exports = ar: require './ar' # العربية, Arabic pt: require './pt' # português, Portuguese 'pt-BR': require './pt-BR' # português do Brasil, Portuguese (Brazil) - 'pt-PT': require './pt-PT' # Português europeu, Portuguese (Portugal) + 'pt-PT': require './pt-PT' # Português (Portugal), Portuguese (Portugal) pl: require './pl' # język polski, Polish it: require './it' # italiano, Italian tr: require './tr' # Türkçe, Turkish @@ -58,3 +58,4 @@ module.exports = hi: require './hi' # मानक हिन्दी, Hindi ur: require './ur' # اُردُو, Urdu ms: require './ms' # Bahasa Melayu, Bahasa Malaysia + ca: require './ca' # Català, Catalan diff --git a/app/locale/pt-PT.coffee b/app/locale/pt-PT.coffee index bd6e4fbce..1ae60b284 100644 --- a/app/locale/pt-PT.coffee +++ b/app/locale/pt-PT.coffee @@ -213,10 +213,10 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription: # active: "Looking for interview offers now" # inactive: "Not looking for offers right now" # complete: "complete" -# next: "Next" -# next_city: "city?" + next: "Seguinte" + next_city: "cidade?" # next_country: "pick your country." -# next_name: "name?" + next_name: "nome?" # next_short_description: "write a short description." # next_long_description: "describe your desired position." # next_skills: "list at least five skills." @@ -226,39 +226,39 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription: # next_links: "add any personal or social links." # next_photo: "add an optional professional photo." # next_active: "mark yourself open to offers to show up in searches." -# example_blog: "Blog" -# example_personal_site: "Personal Site" -# links_header: "Personal Links" + example_blog: "Blog" + example_personal_site: "Sítio Pessoal" + links_header: "Ligações Pessoais" # links_blurb: "Link any other sites or profiles you want to highlight, like your GitHub, your LinkedIn, or your blog." -# links_name: "Link Name" -# links_name_help: "What are you linking to?" -# links_link_blurb: "Link URL" + links_name: "Nome da Ligação" + links_name_help: "A que é que está a ligar?" + links_link_blurb: "URL da Ligação" # basics_header: "Update basic info" # basics_active: "Open to Offers" # basics_active_help: "Want interview offers right now?" # basics_job_title: "Desired Job Title" # basics_job_title_help: "What role are you looking for?" -# basics_city: "City" + basics_city: "Cidade" # basics_city_help: "City you want to work in (or live in now)." -# basics_country: "Country" + basics_country: "País" # basics_country_help: "Country you want to work in (or live in now)." # basics_visa: "US Work Status" # basics_visa_help: "Are you authorized to work in the US, or do you need visa sponsorship? (If you live in Canada or Australia, mark authorized.)" -# basics_looking_for: "Looking For" -# basics_looking_for_full_time: "Full-time" -# basics_looking_for_part_time: "Part-time" -# basics_looking_for_remote: "Remote" + basics_looking_for: "À Procura De" + basics_looking_for_full_time: "Tempo Inteiro" + basics_looking_for_part_time: "Part-time" + basics_looking_for_remote: "Remoto" # basics_looking_for_contracting: "Contracting" # basics_looking_for_internship: "Internship" # basics_looking_for_help: "What kind of developer position do you want?" # name_header: "Fill in your name" -# name_anonymous: "Anonymous Developer" + name_anonymous: "Desenvolvedor Anónimo" # name_help: "Name you want employers to see, like 'Nick Winter'." # short_description_header: "Write a short description of yourself" # short_description_blurb: "Add a tagline to help an employer quickly learn more about you." # short_description: "Tagline" # short_description_help: "Who are you, and what are you looking for? 140 characters max." -# skills_header: "Skills" + skills_header: "Habilidades" # skills_help: "Tag relevant developer skills in order of proficiency." # long_description_header: "Describe your desired position" # long_description_blurb: "Tell employers how awesome you are and what role you want." @@ -266,22 +266,22 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription: # long_description_help: "Describe yourself to potential employers. Keep it short and to the point. We recommend outlining the position that would most interest you. Tasteful markdown okay; 600 characters max." # work_experience: "Work Experience" # work_header: "Chronicle your work history" -# work_years: "Years of Experience" + work_years: "Anos de Experiência" # work_years_help: "How many years of professional experience (getting paid) developing software do you have?" # work_blurb: "List your relevant work experience, most recent first." -# work_employer: "Employer" -# work_employer_help: "Name of your employer." -# work_role: "Job Title" + work_employer: "Empregador" + work_employer_help: "Nome do seu empregador." + work_role: "Título do Emprego" # work_role_help: "What was your job title or role?" -# work_duration: "Duration" + work_duration: "Duração" # work_duration_help: "When did you hold this gig?" -# work_description: "Description" + work_description: "Descrição" # work_description_help: "What did you do there? (140 chars; optional)" -# education: "Education" + education: "Educação" # education_header: "Recount your academic ordeals" # education_blurb: "List your academic ordeals." -# education_school: "School" -# education_school_help: "Name of your school." + education_school: "Escola" + education_school_help: "Nome da sua escola." # education_degree: "Degree" # education_degree_help: "What was your degree and field of study?" # education_duration: "Dates" @@ -312,18 +312,18 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription: filter_visa: "Visa" filter_visa_yes: "Autorizado Para Trabalhar Nos EUA" filter_visa_no: "Não Autorizado" -# filter_education_top: "Top School" -# filter_education_other: "Other" -# filter_role_web_developer: "Web Developer" -# filter_role_software_developer: "Software Developer" -# filter_role_mobile_developer: "Mobile Developer" -# filter_experience: "Experience" -# filter_experience_senior: "Senior" -# filter_experience_junior: "Junior" + filter_education_top: "Universidade" + filter_education_other: "Outro" + filter_role_web_developer: "Desenvolvedor da Web" + filter_role_software_developer: "Desenvolvedor de Software" + filter_role_mobile_developer: "Desenvolvedor Mobile" + filter_experience: "Experiência" + filter_experience_senior: "Sénior" + filter_experience_junior: "Júnior" # filter_experience_recent_grad: "Recent Grad" -# filter_experience_student: "College Student" -# filter_results: "results" -# start_hiring: "Start hiring." + filter_experience_student: "Estudante Universitário" + filter_results: "resultados" + start_hiring: "Começar a contratar." # reasons: "Three reasons you should hire through us:" # everyone_looking: "Everyone here is looking for their next opportunity." # everyone_looking_blurb: "Forget about 20% LinkedIn InMail response rates. Everyone that we list on this site wants to find their next position and will respond to your request for an introduction." @@ -408,11 +408,11 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription: tip_morale_improves: "O carregamento irá continuar até que a moral melhore." tip_all_species: "Acreditamos em oportunidades iguais para todas as espécies, em relação a aprenderem a programar." tip_reticulating: "A reticular espinhas." - tip_harry: "Você é um Feitiçeiro, " -# tip_great_responsibility: "With great coding skill comes great debug responsibility." -# tip_munchkin: "If you don't eat your vegetables, a munchkin will come after you while you're asleep." + tip_harry: "Você é um Feiticeiro, " + tip_great_responsibility: "Com uma grande habilidade de programação vem uma grande responsabilidade de depuração." + tip_munchkin: "Se não comer os seus vegetais, virá um ogre atrás de si enquanto estiver a dormir." tip_binary: "Há apenas 10 tipos de pessoas no mundo: aquelas que percebem binário e aquelas que não." -# tip_commitment_yoda: "A programmer must have the deepest commitment, the most serious mind. ~ Yoda" + tip_commitment_yoda: "Um programador deve ter o compromisso mais profundo, a mente mais séria. ~ Yoda" tip_no_try: "Fazer. Ou não fazer. Não há nenhum tentar. - Yoda" tip_patience: "Paciência tu deves ter, jovem Padawan. - Yoda" tip_documented_bug: "Um erro documentado não é um erro; é uma funcionalidade." @@ -448,7 +448,7 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription: # temp: "Temp" save_load: -# granularity_saved_games: "Saved" + granularity_saved_games: "Guardados" granularity_change_history: "Histórico" options: @@ -504,7 +504,7 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription: # toggle_grid: "Toggle grid overlay." # toggle_pathfinding: "Toggle pathfinding overlay." # beautify: "Beautify your code by standardizing its formatting." - move_wizard: "Mover o seu Feitiçeiro pelo nível." + move_wizard: "Mover o seu Feiticeiro pelo nível." admin: av_title: "Vistas de Administrador" @@ -547,7 +547,7 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription: grassy: "Com Relva" fork_title: "Bifurcar Nova Versão" fork_creating: "A Criar Bifurcação..." -# randomize: "Randomize" + randomize: "Randomizar" more: "Mais" wiki: "Wiki" live_chat: "Chat Ao Vivo" @@ -561,8 +561,8 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription: level_tab_thangs_all: "Todos" level_tab_thangs_conditions: "Condições Iniciais" level_tab_thangs_add: "Adicionar Thangs" -# delete: "Delete" -# duplicate: "Duplicate" + delete: "Eliminar" + duplicate: "Duplicar" level_settings_title: "Configurações" level_component_tab_title: "Componentes Atuais" level_component_btn_new: "Criar Novo Componente" @@ -572,7 +572,7 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription: level_components_title: "Voltar para Todos os Thangs" level_components_type: "Tipo" level_component_edit_title: "Editar Componente" -# level_component_config_schema: "Config Schema" + level_component_config_schema: "Configurar Esquema" level_component_settings: "Configurações" level_system_edit_title: "Editar Sistema" create_system_title: "Criar Novo Sistema" @@ -632,22 +632,22 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription: who_description_prefix: "começaram juntos o CodeCombat em 2013. Também criaram o " who_description_suffix: "em 2008, tornando-o a aplicação nº1 da web e iOS para aprender a escrever caracteteres Chineses e Japoneses." who_description_ending: "Agora, está na altura de ensinar as pessoas a escrever código." -# why_paragraph_1: "When making Skritter, George didn't know how to program and was constantly frustrated by his inability to implement his ideas. Afterwards, he tried learning, but the lessons were too slow. His housemate, wanting to reskill and stop teaching, tried Codecademy, but \"got bored.\" Each week another friend started Codecademy, then dropped off. We realized it was the same problem we'd solved with Skritter: people learning a skill via slow, intensive lessons when what they need is fast, extensive practice. We know how to fix that." -# why_paragraph_2: "Need to learn to code? You don't need lessons. You need to write a lot of code and have a great time doing it." -# why_paragraph_3_prefix: "That's what programming is about. It's gotta be fun. Not fun like" -# why_paragraph_3_italic: "yay a badge" -# why_paragraph_3_center: "but fun like" -# why_paragraph_3_italic_caps: "NO MOM I HAVE TO FINISH THE LEVEL!" -# why_paragraph_3_suffix: "That's why CodeCombat is a multiplayer game, not a gamified lesson course. We won't stop until you can't stop--but this time, that's a good thing." -# why_paragraph_4: "If you're going to get addicted to some game, get addicted to this one and become one of the wizards of the tech age." -# why_ending: "And hey, it's free. " -# why_ending_url: "Start wizarding now!" -# george_description: "CEO, business guy, web designer, game designer, and champion of beginning programmers everywhere." -# scott_description: "Programmer extraordinaire, software architect, kitchen wizard, and master of finances. Scott is the reasonable one." -# nick_description: "Programming wizard, eccentric motivation mage, and upside-down experimenter. Nick can do anything and chooses to build CodeCombat." -# jeremy_description: "Customer support mage, usability tester, and community organizer; you've probably already spoken with Jeremy." -# michael_description: "Programmer, sys-admin, and undergrad technical wunderkind, Michael is the person keeping our servers online." -# glen_description: "Programmer and passionate game developer, with the motivation to make this world a better place, by developing things that matter. The word impossible can't be found in his dictionary. Learning new skills is his joy!" + why_paragraph_1: "Aquando da conceção do Skritter, o George não sabia programar e estava constantemente frustrado devido à sua inabilidade para implementar as ideias dele. Mais tarde, tentou aprender, mas as aulas eram muito lentas. O seu colega de quarto, numa tentativa de melhorar as suas habilidades e parar de ensinar, tentou o Codecademy, mas \"aborreceu-se.\" A cada semana, um outro amigo começava no Codecademy, mas desistia sempre. Apercebemo-nos de que era o mesmo problema que resolveríamos com o Skritter: pessoas a aprender uma habilidade através de aulas lentas e intensivas, quando o que precisam é de praticar rápida e extensivamente. Nós sabemos como resolver isso." + why_paragraph_2: "Precisa de aprender a programar? Não precisa de aulas. Precisa sim de escrever muito código e passar um bom bocado enquanto o faz." + why_paragraph_3_prefix: "Afinal, é sobre isso que é a programação. Tem de ser divertida. Não divertida do género" + why_paragraph_3_italic: "yay uma medalha" + why_paragraph_3_center: "mas sim divertida do género" + why_paragraph_3_italic_caps: "NÃO MÃE, TENHO DE ACABAR O NÍVEL!" + why_paragraph_3_suffix: "É por isso que o CodeCombat é um jogo multijogador, e não um jogo que não passa de um curso com lições. Nós não vamos parar enquanto não puderes parar--mas desta vez, isso é uma coisa boa." + why_paragraph_4: "Se vais ficar viciado em algum jogo, vicia-te neste e torna-te num dos feiticeiros da idade da tecnologia." + why_ending: "E vejam só, é gratuito. " + why_ending_url: "Comece a enfeitiçar agora!" + george_description: "CEO, homem de negócios, designer da web, designer de jogos e campeão dos programadores iniciantes de todo o lado." + scott_description: "Programador extraordinário, arquiteto de software, feiticeiro da cozinha e mestre das finanças. O Scott é sensato." + nick_description: "Feiticeiro da programção, mago da motivação excêntrico e experimentador de pernas para o ar. O Nick pode fazer qualquer coisa e escolhe construir o CodeCombat." + jeremy_description: "Mago do suporte ao cliente, testador do uso e organizador da comunidade; provavelmente já falou com o Jeremy." + michael_description: "Programador, administrador do sistema e técnico de graduação prodígio, o Michael é a pessoa que mantém os nossos servidores online." + glen_description: "Programador e desenvolvedor de jogos apaixonado, com a motivação necessária para tornar este mundo um lugar melhor, ao desenvolver coisas que importam. A palavra impossível não pode ser encontrada no dicionário dele. Aprender novas habilidades é a alegria dele!" legal: page_title: "Legal" @@ -834,11 +834,11 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription: rank_failed: "Falhou a Classificar" rank_being_ranked: "Jogo a ser Classificado" # rank_last_submitted: "submitted " -# help_simulate: "Help simulate games?" - code_being_simulated: "O teu código está a ser simulado por outros jogadores, para ser classificado. Isto será actualizado quando surgirem novas partidas." + help_simulate: "Ajudar a simular jogos?" + code_being_simulated: "O seu novo código está a ser simulado por outros jogadores, para ser classificado. Isto será atualizado quando surgirem novas partidas." no_ranked_matches_pre: "Sem jogos classificados pela equipa " no_ranked_matches_post: "! Joga contra alguns adversários e volta aqui para veres o teu jogo classificado." - choose_opponent: "Escolhe um Adversário" + choose_opponent: "Escolha um Adversário" select_your_language: "Selecione a sua linguagem!" tutorial_play: "Jogar Tutorial" tutorial_recommended: "Recomendado se nunca jogou antes" @@ -848,40 +848,40 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription: simple_ai: "Inteligência Artificial Simples" warmup: "Aquecimento" vs: "VS" -# friends_playing: "Friends Playing" -# log_in_for_friends: "Log in 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" -# tournament_ends: "Tournament ends" -# tournament_ended: "Tournament ended" -# tournament_rules: "Tournament Rules" -# tournament_blurb: "Write code, collect gold, build armies, crush foes, win prizes, and upgrade your career in our $40,000 Greed tournament! Check out the details" -# tournament_blurb_blog: "on our blog" + friends_playing: "Amigos a Jogar" + log_in_for_friends: "Inicie sessão para jogar com os seus amigos!" + social_connect_blurb: "Conecte-se e jogue contra os seus amigos!" + invite_friends_to_battle: "Convide os seus amigos para se juntarem a si em batalha!" + fight: "Lutar!" + watch_victory: "Veja a sua vitória" + defeat_the: "Derrote o" + tournament_ends: "O Torneio acaba" + tournament_ended: "O Torneio acabou" + tournament_rules: "Regras do Torneio" + tournament_blurb: "Escreva código, recolha ouro, construa exércitos, esmague inimigos, ganhe prémios e melhore a sua carreira no nosso torneio $40,000 Greed! Confira os detalhes" + tournament_blurb_blog: "no nosso blog" rules: "Regras" winners: "Vencedores" -# ladder_prizes: -# title: "Tournament Prizes" -# blurb_1: "These prizes will be awarded according to" -# blurb_2: "the tournament rules" -# blurb_3: "to the top human and ogre players." -# blurb_4: "Two teams means double the prizes!" -# blurb_5: "(There will be two first place winners, two second-place winners, etc.)" -# rank: "Rank" -# prizes: "Prizes" -# total_value: "Total Value" -# in_cash: "in cash" -# custom_wizard: "Custom CodeCombat Wizard" -# custom_avatar: "Custom CodeCombat avatar" -# heap: "for six months of \"Startup\" access" -# credits: "credits" -# one_month_coupon: "coupon: choose either Rails or HTML" -# one_month_discount: "discount, 30% off: choose either Rails or HTML" -# license: "license" -# oreilly: "ebook of your choice" + ladder_prizes: + title: "Prémios do Torneio" + blurb_1: "Estes prémios serão entregues de acordo com" + blurb_2: "as regras do torneio" + blurb_3: "aos melhores jogadores humanos e ogres." + blurb_4: "Duas equipas significam o dobro dos prémios!" + blurb_5: "(Haverá dois vencedores em primeiro lugar, dois em segundo, etc.)" + rank: "Classificação" + prizes: "Prémios" + total_value: "Valor Total" + in_cash: "em dinheiro" + custom_wizard: "Um Feiticeiro do CodeCombat Personalizado" + custom_avatar: "Um Avatar do CodeCombat Personalizado" + heap: "para seis meses de acesso \"Startup\"" + credits: "créditos" + one_month_coupon: "cupão: escolha Rails ou HTML" + one_month_discount: "desconto de 30%: escolha Rails ou HTML" + license: "licença" + oreilly: "ebook à sua escolha" loading_error: could_not_load: "Erro ao carregar do servidor" @@ -909,8 +909,8 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription: # user_schema: "User Schema" # user_profile: "User Profile" # patches: "Patches" -# patched_model: "Source Document" -# model: "Model" + patched_model: "Documento Fonte" + model: "Modelo" system: "Sistema" component: "Componente" components: "Componentes" @@ -921,20 +921,20 @@ module.exports = nativeDescription: "Português (Portugal)", englishDescription: article: "Artigo" # user_names: "User Names" # thang_names: "Thang Names" -# files: "Files" + files: "Ficheiros" # top_simulators: "Top Simulators" -# source_document: "Source Document" + source_document: "Documento Fonte" document: "Documento" # sprite_sheet: "Sprite Sheet" # candidate_sessions: "Candidate Sessions" # user_remark: "User Remark" versions: "Versões" -# delta: -# added: "Added" -# modified: "Modified" -# deleted: "Deleted" -# moved_index: "Moved Index" -# text_diff: "Text Diff" -# merge_conflict_with: "MERGE CONFLICT WITH" -# no_changes: "No Changes" + delta: + added: "Adicionados/as" + modified: "Modificados/as" + deleted: "Eliminados/as" + moved_index: "Índice Movido" + text_diff: "Diferença de Texto" + merge_conflict_with: "FUNDIR CONFLITO COM" + no_changes: "Sem Alterações" diff --git a/app/models/Achievement.coffee b/app/models/Achievement.coffee index 88603c6ef..723b2b0bf 100644 --- a/app/models/Achievement.coffee +++ b/app/models/Achievement.coffee @@ -1,5 +1,5 @@ CocoModel = require './CocoModel' -util = require '../lib/utils' +utils = require '../lib/utils' module.exports = class Achievement extends CocoModel @className: 'Achievement' @@ -11,6 +11,47 @@ module.exports = class Achievement extends CocoModel # TODO logic is duplicated in Mongoose Achievement schema getExpFunction: -> - kind = @get('function')?.kind or @schema.function.default.kind - parameters = @get('function')?.parameters or @schema.function.default.parameters + kind = @get('function')?.kind or jsonschema.properties.function.default.kind + parameters = @get('function')?.parameters or jsonschema.properties.function.default.parameters return utils.functionCreators[kind](parameters) if kind of utils.functionCreators + + @styleMapping: + 1: 'achievement-wood' + 2: 'achievement-stone' + 3: 'achievement-silver' + 4: 'achievement-gold' + 5: 'achievement-diamond' + + getStyle: -> Achievement.styleMapping[@get 'difficulty'] + + @defaultImageURL: '/images/achievements/default.png' + + getImageURL: -> + if @get 'icon' then '/file/' + @get('icon') else Achievement.defaultImageURL + + hasImage: -> @get('icon')? + + # TODO Could cache the default icon separately + cacheLockedImage: -> + return @lockedImageURL if @lockedImageURL + canvas = document.createElement 'canvas' + image = new Image + image.src = @getImageURL() + defer = $.Deferred() + image.onload = => + canvas.width = image.width + canvas.height = image.height + context = canvas.getContext '2d' + context.drawImage image, 0, 0 + imgData = context.getImageData 0, 0, canvas.width, canvas.height + imgData = utils.grayscale imgData + context.putImageData imgData, 0, 0 + @lockedImageURL = canvas.toDataURL() + defer.resolve @lockedImageURL + defer + + getLockedImageURL: -> @lockedImageURL + + i18nName: -> utils.i18n @attributes, 'name' + + i18nDescription: -> utils.i18n @attributes, 'description' diff --git a/app/models/CocoModel.coffee b/app/models/CocoModel.coffee index c2a4c0fe7..de02d9f1d 100644 --- a/app/models/CocoModel.coffee +++ b/app/models/CocoModel.coffee @@ -1,8 +1,6 @@ storage = require 'lib/storage' deltasLib = require 'lib/deltas' -NewAchievementCollection = require '../collections/NewAchievementCollection' - class CocoModel extends Backbone.Model idAttribute: '_id' loaded: false @@ -301,11 +299,13 @@ class CocoModel extends Backbone.Model return if _.isString @url then @url else @url() @pollAchievements: -> + NewAchievementCollection = require '../collections/NewAchievementCollection' # Nasty mutual inclusion if put on top achievements = new NewAchievementCollection - achievements.fetch( + achievements.fetch success: (collection) -> me.fetch (success: -> Backbone.Mediator.publish('achievements:new', collection)) unless _.isEmpty(collection.models) - ) + error: -> + console.error 'Miserably failed to fetch unnotified achievements', arguments CocoModel.pollAchievements = _.debounce CocoModel.pollAchievements, 500 diff --git a/app/models/EarnedAchievement.coffee b/app/models/EarnedAchievement.coffee new file mode 100644 index 000000000..2fa36037c --- /dev/null +++ b/app/models/EarnedAchievement.coffee @@ -0,0 +1,7 @@ +CocoModel = require './CocoModel' +utils = require '../lib/utils' + +module.exports = class EarnedAchievement extends CocoModel + @className: 'EarnedAchievement' + @schema: require 'schemas/models/earned_achievement' + urlRoot: '/db/earnedachievement' diff --git a/app/models/Level.coffee b/app/models/Level.coffee index 4f95d269a..d2972366b 100644 --- a/app/models/Level.coffee +++ b/app/models/Level.coffee @@ -80,7 +80,7 @@ module.exports = class Level extends CocoModel visit = (c) -> return if c in sorted lc = _.find levelComponents, {original: c.original} - console.error thang.id, 'couldn\'t find lc for', c, 'of', levelComponents unless lc + console.error thang.id or thang.name, 'couldn\'t find lc for', c, 'of', levelComponents unless lc return unless lc if lc.name is 'Programmable' # Programmable always comes last @@ -88,7 +88,7 @@ module.exports = class Level extends CocoModel else for d in lc.dependencies or [] c2 = _.find thang.components, {original: d.original} - console.error thang.id, 'couldn\'t find dependent Component', d.original, 'from', lc.name unless c2 + console.error thang.id or thang.name, 'couldn\'t find dependent Component', d.original, 'from', lc.name unless c2 visit c2 if c2 if lc.name is 'Collides' allied = _.find levelComponents, {name: 'Allied'} diff --git a/app/models/LevelSession.coffee b/app/models/LevelSession.coffee index b98cac66e..34b03be6d 100644 --- a/app/models/LevelSession.coffee +++ b/app/models/LevelSession.coffee @@ -36,3 +36,9 @@ module.exports = class LevelSession extends CocoModel spell = item[1] return true if c1[thang][spell] isnt c2[thang]?[spell] false + + isMultiplayer: -> + @get('team')? # Only multiplayer level sessions have teams defined + + completed: -> + @get('state')?.complete || false diff --git a/app/models/SuperModel.coffee b/app/models/SuperModel.coffee index 24f9b3e04..cd7db688b 100644 --- a/app/models/SuperModel.coffee +++ b/app/models/SuperModel.coffee @@ -58,9 +58,15 @@ module.exports = class SuperModel extends Backbone.Model return res else @addCollection collection - @listenToOnce collection, 'sync', (c) -> - console.debug 'Registering collection', url - @registerCollection c + onCollectionSynced = (c) -> + if collection.url is c.url + console.debug 'Registering collection', url, c + @registerCollection c + else + console.warn 'Sync triggered for collection', c + console.warn 'Yet got other object', c + @listenToOnce collection, 'sync', onCollectionSynced + @listenToOnce collection, 'sync', onCollectionSynced res = @addModelResource(collection, name, fetchOptions, value) res.load() if not (res.isLoading or res.isLoaded) return res diff --git a/app/models/User.coffee b/app/models/User.coffee index 91cc40e49..7b492cdba 100644 --- a/app/models/User.coffee +++ b/app/models/User.coffee @@ -9,14 +9,24 @@ module.exports = class User extends CocoModel urlRoot: '/db/user' notyErrors: false + defaults: + points: 0 + initialize: -> super() @migrateEmails() + onLoaded: -> + CocoModel.pollAchievements() # Check for achievements on login + super arguments... + isAdmin: -> permissions = @attributes['permissions'] or [] return 'admin' in permissions + isAnonymous: -> + @get 'anonymous' + displayName: -> @get('name') or 'Anoner' @@ -32,47 +42,13 @@ module.exports = class User extends CocoModel return "/file/#{photoURL}#{prefix}s=#{size}" return "/db/user/#{@id}/avatar?s=#{size}&employerPageAvatar=#{useEmployerPageAvatar}" - @getByID = (id, properties, force) -> - {me} = require 'lib/auth' - return me if me.id is id - user = cache[id] or new module.exports({_id: id}) - if force or not cache[id] - user.loading = true - user.fetch( - success: -> - user.loading = false - Backbone.Mediator.publish('user:fetched') - #user.trigger 'sync' # needed? - ) - cache[id] = user - user + getSlugOrID: -> @get('slug') or @get('_id') + set: -> if arguments[0] is 'jobProfileApproved' and @get("jobProfileApproved") is false and not @get("jobProfileApprovedDate") @set "jobProfileApprovedDate", (new Date()).toISOString() super arguments... - # callbacks can be either success or error - @getByIDOrSlug: (idOrSlug, force, callbacks={}) -> - {me} = require 'lib/auth' - isID = util.isID idOrSlug - if me.id is idOrSlug or me.slug is idOrSlug - callbacks.success me if callbacks.success? - return me - cached = cache[idOrSlug] - user = cached or new @ _id: idOrSlug - if force or not cached - user.loading = true - user.fetch - success: -> - user.loading = false - Backbone.Mediator.publish 'user:fetched' - callbacks.success user if callbacks.success? - error: -> - user.loading = false - callbacks.error user if callbacks.error? - cache[idOrSlug] = user - user - @getUnconflictedName: (name, done) -> $.ajax "/auth/name/#{name}", success: (data) -> done data.name @@ -111,19 +87,16 @@ module.exports = class User extends CocoModel isEmailSubscriptionEnabled: (name) -> (@get('emails') or {})[name]?.enabled a = 5 - b = 40 + b = 100 + c = b - # y = a * ln(1/b * (x + b)) + 1 + # y = a * ln(1/b * (x + c)) + 1 @levelFromExp: (xp) -> - if xp > 0 then Math.floor(a * Math.log((1/b) * (xp + b))) + 1 else 1 + if xp > 0 then Math.floor(a * Math.log((1/b) * (xp + c))) + 1 else 1 - # x = (e^((y-1)/a) - 1) * b + # x = b * e^((y-1)/a) - c @expForLevel: (level) -> - Math.ceil((Math.exp((level - 1)/ a) - 1) * b) + if level > 1 then Math.ceil Math.exp((level - 1)/ a) * b - c else 0 level: -> User.levelFromExp(@get('points')) - - levelFromExp: (xp) -> User.levelFromExp(xp) - - expForLevel: (level) -> User.expForLevel(level) diff --git a/app/schemas/models/achievement.coffee b/app/schemas/models/achievement.coffee index 196c81974..473e53cc8 100644 --- a/app/schemas/models/achievement.coffee +++ b/app/schemas/models/achievement.coffee @@ -24,8 +24,9 @@ MongoFindQuerySchema = '^[-a-zA-Z0-9\.]*$': oneOf: [ #{$ref: '#/definitions/' + MongoQueryOperatorSchema.id}, - {type: 'string'}, + {type: 'string'} {type: 'object'} + {type: 'boolean'} ] additionalProperties: true # TODO make Treema accept new pattern matched keys definitions: {} @@ -34,36 +35,58 @@ MongoFindQuerySchema.definitions[MongoQueryOperatorSchema.id] = MongoQueryOperat AchievementSchema = c.object() c.extendNamedProperties AchievementSchema -c.extendBasicProperties AchievementSchema, 'article' +c.extendBasicProperties AchievementSchema, 'achievement' c.extendSearchableProperties AchievementSchema -_.extend(AchievementSchema.properties, +_.extend AchievementSchema.properties, query: #type:'object' $ref: '#/definitions/' + MongoFindQuerySchema.id - worth: {type: 'number'} + worth: c.float + default: 10 collection: {type: 'string'} - description: {type: 'string'} - userField: {type: 'string'} + description: c.shortString + default: 'Probably the coolest you\'ll ever get.' + userField: c.shortString() related: c.objectId(description: 'Related entity') icon: {type: 'string', format: 'image-file', title: 'Icon'} + category: + enum: ['level', 'ladder', 'contributor'] + description: 'For categorizing and display purposes' + difficulty: c.int + description: 'The higher the more difficult' + default: 1 proportionalTo: type: 'string' description: 'For repeatables only. Denotes the field a repeatable achievement needs for its calculations' + recalculable: + type: 'boolean' + description: 'Needs to be set to true before it is elligible for recalculation.' + default: true function: type: 'object' + description: 'Function that gives total experience for X amount achieved' properties: - kind: {enum: ['linear', 'logarithmic'], default: 'linear'} + kind: {enum: ['linear', 'logarithmic', 'quadratic'], default: 'linear'} parameters: type: 'object' properties: a: {type: 'number', default: 1} b: {type: 'number', default: 1} c: {type: 'number', default: 1} + additionalProperties: true default: {kind: 'linear', parameters: a: 1} required: ['kind', 'parameters'] additionalProperties: false -) + i18n: c.object + format: 'i18n' + props: ['name', 'description'] + description: 'Help translate this achievement' + +_.extend AchievementSchema, # Let's have these on the bottom + # TODO We really need some required properties in my opinion but this makes creating new achievements impossible as it is now + #required: ['name', 'description', 'query', 'worth', 'collection', 'userField', 'category', 'difficulty'] + additionalProperties: false AchievementSchema.definitions = {} AchievementSchema.definitions[MongoFindQuerySchema.id] = MongoFindQuerySchema diff --git a/app/schemas/models/level_session.coffee b/app/schemas/models/level_session.coffee index 284d98fc0..575937993 100644 --- a/app/schemas/models/level_session.coffee +++ b/app/schemas/models/level_session.coffee @@ -101,6 +101,14 @@ _.extend LevelSessionSchema.properties, type: 'object' source: type: 'string' + goalStates: + type: 'object' + description: 'Maps Goal ID on a goal state object' + additionalProperties: + title: 'Goal State' + type: 'object' + properties: + status: enum: ['failure', 'incomplete', 'success'] code: type: 'object' diff --git a/app/schemas/models/patch.coffee b/app/schemas/models/patch.coffee index 6af348725..bd4e822fe 100644 --- a/app/schemas/models/patch.coffee +++ b/app/schemas/models/patch.coffee @@ -20,6 +20,9 @@ PatchSchema = c.object({title: 'Patch', required: ['target', 'delta', 'commitMes major: {type: 'number', minimum: 0} minor: {type: 'number', minimum: 0} }) + + wasPending: type: 'boolean' + newlyAccepted: type: 'boolean' }) c.extendBasicProperties(PatchSchema, 'patch') diff --git a/app/schemas/models/user.coffee b/app/schemas/models/user.coffee index 47a164b5c..be4478820 100644 --- a/app/schemas/models/user.coffee +++ b/app/schemas/models/user.coffee @@ -222,6 +222,33 @@ _.extend UserSchema.properties, points: {type: 'number'} activity: {type: 'object', description: 'Summary statistics about user activity', additionalProperties: c.activity} + stats: c.object {additionalProperties: false}, + gamesCompleted: c.int() + articleEdits: c.int() + levelEdits: c.int() + levelSystemEdits: c.int() + levelComponentEdits: c.int() + thangTypeEdits: c.int() + patchesSubmitted: c.int + description: 'Amount of patches submitted, not necessarily accepted' + patchesContributed: c.int + description: 'Amount of patches submitted and accepted' + patchesAccepted: c.int + description: 'Amount of patches accepted by the user as owner' + # The below patches only apply to those that actually got accepted + totalTranslationPatches: c.int() + totalMiscPatches: c.int() + articleTranslationPatches: c.int() + articleMiscPatches: c.int() + levelTranslationPatches: c.int() + levelMiscPatches: c.int() + levelComponentTranslationPatches: c.int() + levelComponentMiscPatches: c.int() + levelSystemTranslationPatches: c.int() + levelSystemMiscPatches: c.int() + thangTypeTranslationPatches: c.int() + thangTypeMiscPatches: c.int() + c.extendBasicProperties UserSchema, 'user' diff --git a/app/schemas/schemas.coffee b/app/schemas/schemas.coffee index 65045a2e7..ea6881729 100644 --- a/app/schemas/schemas.coffee +++ b/app/schemas/schemas.coffee @@ -19,6 +19,8 @@ me.date = (ext) -> combine({type: ['object', 'string'], format: 'date-time'}, ex # should just be string (Mongo ID), but sometimes mongoose turns them into objects representing those, so we are lenient me.objectId = (ext) -> schema = combine({type: ['object', 'string']}, ext) me.url = (ext) -> combine({type: 'string', format: 'url', pattern: urlPattern}, ext) +me.int = (ext) -> combine {type: 'integer'}, ext +me.float = (ext) -> combine {type: 'number'}, ext PointSchema = me.object {title: 'Point', description: 'An {x, y} coordinate point.', format: 'point2d', required: ['x', 'y']}, x: {title: 'x', description: 'The x coordinate.', type: 'number', 'default': 15} diff --git a/app/styles/account/account_home.sass b/app/styles/account/account_home.sass new file mode 100644 index 000000000..800c8a8b2 --- /dev/null +++ b/app/styles/account/account_home.sass @@ -0,0 +1,32 @@ +@import "../bootstrap/variables" +@import "../bootstrap/mixins" + +#account-home + dl + margin-bottom: 0px + + img#picture + max-width: 100% + + .panel + margin-bottom: 10px + + h2 + margin-bottom: 0px + + a + font-size: 28px + margin-left: 5px + + .panel-title > a + margin-left: 5px + color: rgb(11, 99, 188) + + .panel-me + td + padding-left: 15px + + .panel-emails + h4 + font-family: $font-family-base + diff --git a/app/styles/achievements.sass b/app/styles/achievements.sass new file mode 100644 index 000000000..39a277aa4 --- /dev/null +++ b/app/styles/achievements.sass @@ -0,0 +1,243 @@ +@import 'bootstrap/variables' + +.achievement-body + position: relative + + .achievement-icon + position: absolute + + .achievement-image + width: 100% + height: 100% + img + position: absolute + margin: auto + top: 0 + left: 0 + right: 0 + bottom: 0 + + &.locked + .achievement-content + background-image: url("/images/achievements/achievement_background_locked.png") + &:not(.locked) + .achievement-content + background-image: url("/images/achievements/achievement_background_light.png") + + .achievement-content + background-size: 100% 100% + text-align: center + overflow: hidden + + > .achievement-title + font-family: $font-family-base + font-weight: bold + white-space: nowrap + max-height: 2em + overflow: hidden + text-overflow: ellipsis + + + > .achievement-description + white-space: initial + font-size: 12px + line-height: 1.3em + max-height: 2.6em + margin-top: auto + margin-bottom: 0px !important + padding-left: 5px + overflow: hidden + text-overflow: ellipsis + +// Specific to the user stats page +#user-achievements-view + .achievement-body + width: 335px + height: 120px + margin: 10px 0px + + .achievement-icon + width: 120px + height: 120px + top: -10px + + .achievement-image + img + -moz-transform: scale(0.6) + -webkit-transform: scale(0.6) + transform: scale(0.6) + + .achievement-content + margin-left: 60px + margin-right: 5px + width: 260px + height: 100px + padding: 15px 10px 20px 60px + + .achievement-title + font-size: 20px + + .achievement-description + font-size: 12px + line-height: 1.3em + max-height: 2.6em + +.achievement-popup + padding: 20px 0px + position: relative + + .achievement-body + .achievement-icon + z-index: 1000 + width: 200px + height: 200px + left: -140px + top: -20px + + .achievement-image + img + position: absolute + margin: auto + top: 0 + left: 0 + right: 0 + bottom: 0 + + .achievement-content + background-image: url("/images/achievements/achievement_background.png") + position: relative + width: 450px + height: 160px + padding: 24px 30px 20px 60px + + .achievement-title + font-family: Bangers + font-size: 28px + padding-left: -50px + + .achievement-description + font-size: 15px + line-height: 1.3em + max-height: 2.6em + margin-top: auto + margin-bottom: 0px !important + + .progress-wrapper + margin-left: 20px + position: absolute + bottom: 48px + + .user-level + font-size: 20px + color: white + position: absolute + left: -15px + margin-top: -8px + vertical-align: middle + z-index: 1000 + + > .progress-bar-wrapper + position: absolute + margin-left: 17px + width: 314px + height: 20px + z-index: 2 + + > .progress + margin-top: 5px + border-radius: 50px + height: 14px + + > .progress-bar-border + position: absolute + width: 340px + height: 30px + margin-top: -2px + background: url("/images/achievements/bar_border.png") no-repeat + background-size: 100% 100% + z-index: 1 + +.achievement-icon + background-size: 100% 100% !important + +.achievement-wood + &.locked + .achievement-icon + background: url("/images/achievements/border_wood_locked.png") no-repeat + &:not(.locked) + .achievement-icon + background: url("/images/achievements/border_wood.png") no-repeat + +.achievement-stone + &.locked + .achievement-icon + background: url("/images/achievements/border_stone_locked.png") no-repeat + &:not(.locked) + .achievement-icon + background: url("/images/achievements/border_stone.png") no-repeat + +.achievement-silver + &.locked + .achievement-icon + background: url("/images/achievements/border_silver_locked.png") no-repeat + &:not(.locked) + .achievement-icon + background: url("/images/achievements/border_silver.png") no-repeat + +.achievement-gold + &.locked + .achievement-icon + background: url("/images/achievements/border_gold_locked.png") no-repeat + &:not(.locked) + .achievement-icon + background: url("/images/achievements/border_gold.png") no-repeat + +.achievement-diamond + &.locked + .achievement-icon + background: url("/images/achievements/border_diamond_locked.png") no-repeat + &:not(.locked) + .achievement-icon + background: url("/images/achievements/border_diamond.png") no-repeat + +.xp-bar-old + background-color: #680080 + +.xp-bar-new + background-color: #0096ff + +.xp-bar-left + background-color: #fffbfd + +// Achievements page +.achievement-category-title + margin-left: 20px + font-family: $font-family-base + font-weight: bold + color: #5a5a5a + text-transform: uppercase + +.table-layout + #no-achievements + margin-top: 40px + +.achievement-icon-small + height: 18px + +// Achievement Popup +.achievement-popup-container + position: fixed + right: 0px + bottom: 0px + +.popup + cursor: default + left: 600px + +.user-level + background-image: url("/images/achievements/level-bg.png") + width: 38px + height: 38px + line-height: 38px + font-size: 20px + font-family: $font-family-base diff --git a/app/styles/base.sass b/app/styles/base.sass index 259506119..332aaa624 100644 --- a/app/styles/base.sass +++ b/app/styles/base.sass @@ -21,7 +21,8 @@ h1 h2 h3 h4 margin: 56px auto 0 min-height: 600px padding: 14px 12px 5px 12px - @include box-sizing(border-box) + +box-sizing(border-box) + +clearfix() #outer-content-wrapper background: #B4B4B4 @@ -291,3 +292,9 @@ body[lang='ja'] a[data-toggle="coco-modal"] cursor: pointer + +.achievement-corner + position: fixed + bottom: 0px + right: 0px + z-index: 1001 diff --git a/app/styles/common/top_nav.sass b/app/styles/common/top_nav.sass index 3619a3f9b..42e7fce47 100644 --- a/app/styles/common/top_nav.sass +++ b/app/styles/common/top_nav.sass @@ -1,4 +1,44 @@ @import "../bootstrap/variables" +@import "../bootstrap/mixins" + +// This is still very blocky. Browser reflows? Investigate why. +.open > .dropdown-menu + animation-name: fadeAnimation + animation-duration: .7s + animation-iteration-count: 1 + animation-timing-function: ease + animation-fill-mode: forwards + -webkit-animation-name: fadeAnimation + -webkit-animation-duration: .7s + -webkit-animation-iteration-count: 1 + -webkit-animation-timing-function: ease + -webkit-animation-fill-mode: backwards + -moz-animation-name: fadeAnimation + -moz-animation-duration: .7s + -moz-animation-iteration-count: 1 + -moz-animation-timing-function: ease + -moz-animation-fill-mode: forwards + +@keyframes fadeAnimation + from + opacity: 0 + top: 120% + to + opacity: 1 + top: 100% + +@-webkit-keyframes fadeAnimation + from + opacity: 0 + top: 120% + to + opacity: 1 + top: 100% + +a.disabled + color: #5b5855 + text-decoration: none + cursor: default #top-nav a.navbar-brand @@ -19,11 +59,65 @@ .account-settings-image width: 18px height: 18px + margin-right: 5px .glyphicon-user font-size: 16px + margin-right: 5px - .nav.navbar-link-text, .nav.navbar-link-text > li > a + .dropdown + .dropdown-menu + left: auto + width: 280px + padding: 0px + border-radius: 0px + font-family: Bangers + + > .user-dropdown-header + position: relative + background: #E4CF8C + height: 160px + padding: 10px + text-align: center + color: black + border-bottom: #32281e 1px solid + > a:hover + background-color: transparent + img + border: #e3be7a 8px solid + height: 98px // Includes the border + &:hover + box-shadow: 0 0 20px #e3be7a + > h3 + margin-top: 10px + text-shadow: 2px 2px 3px white + color: #31281E + .user-level + position: absolute + top: 73px + right: 86px + color: gold + text-shadow: 1px 1px black, -1px -1px 0 black, 1px -1px 0 black, -1px 1px 0 black + + .user-dropdown-body + color: black + padding: 15px + letter-spacing: 1px + font: 15px 'Helvetica Neue', Helvetica, Arial, sans-serif + +clearfix() + + .user-dropdown-footer + padding: 10px + margin-left: 0px + font-size: 14px + +clearfix() + + .btn-flat + border: #ddd 1px solid + border-radius: 0px + margin: 0px + + .nav.navbar-link-text > li > a font-weight: normal font-size: 25px letter-spacing: 2px @@ -31,7 +125,7 @@ &:hover color: #f8e413 - .navbar-link-text a:hover + .navbar-link-text > li > a:hover background: darken($body-bg, 3%) .btn, .btn-group, .fancy-select @@ -67,9 +161,6 @@ top: 13px max-width: 140px - .nav - margin-bottom: 0 - div.fancy-select text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25) div.trigger diff --git a/app/styles/editor/achievement/edit.sass b/app/styles/editor/achievement/edit.sass index 7177978d3..157353e9a 100644 --- a/app/styles/editor/achievement/edit.sass +++ b/app/styles/editor/achievement/edit.sass @@ -1,4 +1,6 @@ #editor-achievement-edit-view + height: 100% + .treema-root margin: 28px 0px 20px @@ -10,3 +12,9 @@ textarea width: 92% height: 300px + + #achievement-view + min-height: 200px + position: relative + padding-left: 200px + diff --git a/app/styles/editor/related-achievements.sass b/app/styles/editor/related-achievements.sass new file mode 100644 index 000000000..46314ea65 --- /dev/null +++ b/app/styles/editor/related-achievements.sass @@ -0,0 +1,6 @@ +#related-achievements-view + #new-achievement-button + margin-bottom: 10px + + .icon-column + width: 25px diff --git a/app/styles/editor/thang/home.sass b/app/styles/editor/thang/home.sass index 3509c1f27..193ea50d6 100644 --- a/app/styles/editor/thang/home.sass +++ b/app/styles/editor/thang/home.sass @@ -6,4 +6,4 @@ width: 30px td - vertical-align: middle \ No newline at end of file + vertical-align: middle diff --git a/app/styles/editor/thang/thang-type-edit-view.sass b/app/styles/editor/thang/thang-type-edit-view.sass index 135923348..109651a3b 100644 --- a/app/styles/editor/thang/thang-type-edit-view.sass +++ b/app/styles/editor/thang/thang-type-edit-view.sass @@ -77,6 +77,8 @@ background-color: white border-radius: 4px + .play-with-level-input + margin: 5px #spritesheets diff --git a/app/styles/game-menu/options-view.sass b/app/styles/game-menu/options-view.sass index ae9542bff..b796029e6 100644 --- a/app/styles/game-menu/options-view.sass +++ b/app/styles/game-menu/options-view.sass @@ -1,7 +1,7 @@ @import "app/styles/bootstrap/variables" #options-view - .select-group + .select-group, .slider-group display: block min-height: 20px margin-top: 10px @@ -14,6 +14,9 @@ margin-right: 20px margin-bottom: 0 + .slider + width: 200px + .form-group.radio-inline input margin-left: 0px @@ -22,6 +25,7 @@ .radio-inline-parent-label padding-left: 0 + #player-avatar-container position: relative margin: 0px 0px 15px 15px diff --git a/app/styles/notify.sass b/app/styles/notify.sass deleted file mode 100644 index abf50bb70..000000000 --- a/app/styles/notify.sass +++ /dev/null @@ -1,50 +0,0 @@ -.notifyjs-achievement-base - //background: url("/images/pages/base/notify_mockup.png") - background-image: url("/images/pages/base/modal_background.png") - background-size: 100% 100% - width: 500px - height: 200px - padding: 35px 35px 15px 15px - text-align: center - cursor: auto - - .achievement-body - .achievement-image - img - float: left - width: 100px - height: 100px - border-radius: 50% - margin: 20px 30px 20px 30px - -webkit-box-shadow: 0px 0px 36px 0px white - -moz-box-shadow: 0px 0px 36px 0px white - box-shadow: 0px 0px 36px 0px white - - .achievement-title - font-family: Bangers - font-size: 28px - - .achievement-description - margin-top: 10px - font-size: 16px - - .achievement-progress - padding: 15px 0px 0px 0px - - .achievement-message - font-family: Bangers - font-size: 18px - &:empty - display: none - - .progress-wrapper - .progress-bar-wrapper - width: 100% - .earned-exp - padding-left: 5px - font-family: Bangers - font-size: 16px - float: right - -.progress-bar-white - background-color: white diff --git a/app/styles/user/user_home.sass b/app/styles/user/user_home.sass new file mode 100644 index 000000000..ebd67a391 --- /dev/null +++ b/app/styles/user/user_home.sass @@ -0,0 +1,73 @@ +@import "../bootstrap/variables" +@import "../bootstrap/mixins" + +#user-home + margin-top: 20px + + .left-column + +make-sm-column(4) + + .right-column + +make-sm-column(8) + + .profile-wrapper + text-align: center + outline: 1px solid darkgrey + max-width: 100% + +center-block() + + > .picture + width: 100% + background-color: #ffe4bc + border: 4px solid white + + > .profile-info + background: white + + .extra-info + padding-bottom: 3px + &:empty + display: none + + .name + margin: 0px auto + padding: 10px inherit + color: white + text-shadow: 2px 0 0 #000, -2px 0 0 #000, 0 2px 0 #000, 0 -2px 0 #000, 1px 1px #000, -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000 + + .profile-menu + padding-left: 0px + width: 100% + > a + border-radius: 0 + border-width: 1px 0px 0px 0px + border-color: darkgrey + &:hover + border-color: #888 + > span + color: #555555 + font-size: 15px + margin-left: 5px + +.contributor-categories + list-style: none + padding: 0px + margin-top: 15px + + > .contributor-category + outline: 1px solid black + margin-bottom: 15px + + > .contributor-image + border: none + width: 100% + border-bottom: 1px solid black + + > .contributor-title + text-align: center + padding: 5px 0px + margin: 0px + background: white + +.vertical-buffer + padding: 10px 0px diff --git a/app/templates/account/account_home.jade b/app/templates/account/account_home.jade new file mode 100644 index 000000000..d59a411df --- /dev/null +++ b/app/templates/account/account_home.jade @@ -0,0 +1,141 @@ +extends /templates/base + +block content + if !me.isAnonymous() + .clearfix + .col-sm-6.clearfix + h2 + span(data-i18n="account_settings.title") Account Settings + a.spl(href="/account/settings") + i.glyphicon.glyphicon-cog + hr + .row + .col-xs-6 + .panel.panel-default + .panel-heading + h3.panel-title + i.glyphicon.glyphicon-picture + a(href="account/settings#picture" data-i18n="account_settings.picture_tab") Picture + .panel-body.text-center + img#picture(src="#{me.getPhotoURL(150)}" alt="Picture") + .col-xs-6 + .panel.panel-default + .panel-heading + h3.panel-title + i.glyphicon.glyphicon-user + a(href="account/settings#wizard" data-i18n="account_settings.wizard_tab") Wizard + if (wizardSource) + .panel-body.text-center + img(src="#{wizardSource}") + .panel.panel-default.panel-me + .panel-heading + h3.panel-title + i.glyphicon.glyphicon-user + a(href="account/settings#me" data-i18n="account_settings.me_tab") Me + .panel-body + table + tr + th(data-i18n="general.name") Name + td=me.displayName() + tr + th(data-i18n="general.email") Email + td=me.get('email') + .panel.panel-default.panel-emails + .panel-heading + h3.panel-title + i.glyphicon.glyphicon-envelope + a(href="account/settings#emails" data-i18n="account_settings.emails_tab") Emails + .panel-body + if !hasEmailNotes && !hasEmailNews && !hasGeneralNews + p(data-i18n="account_settings.email_subscriptions_none") No email subscriptions. + if hasGeneralNews + h4(data-i18n="account_settings.email_news") News + ul + li(data-i18n="account_settings.email_announcements") Announcements + if hasEmailNotes + h4(data-i18n="account_settings.email_notifications") Notifications + ul + if subs.anyNotes + li(data-i18n="account_settings.email_any_notes") Any Notifications + if subs.recruitNotes + li(data-i18n="account_settings.email_recruit_notes") Job Opportunities + if hasEmailNews + h4(data-i18n="account_settings.contributor_emails") Contributor Emails + ul + if (subs.archmageNews) + li + span(data-i18n="classes.archmage_title") + | Archmage + span(data-i18n="classes.archmage_title_description") + | (Coder) + if (subs.artisanNews) + li + span.spr(data-i18n="classes.artisan_title") + | Artisan + span(data-i18n="classes.artisan_title_description") + | (Level Builder) + if (subs.adventurerNews) + li + span.spr(data-i18n="classes.adventurer_title") + | Adventurer + span(data-i18n="classes.adventurer_title_description") + | (Level Playtester) + if (subs.scribeNews) + li + span.spr(data-i18n="classes.scribe_title") + | Scribe + span(data-i18n="classes.scribe_title_description") + | (Article Editor) + if (subs.diplomatNews) + li + span.spr(data-i18n="classes.diplomat_title") + | Diplomat + span(data-i18n="classes.diplomat_title_description") + | (Translator) + if (subs.ambassadorNews) + li + span.spr(data-i18n="classes.ambassador_title") + | Ambassador + span(data-i18n="classes.ambassador_title_description") + | (Support) + + .panel.panel-default + .panel-heading + h3.panel-title + i.glyphicon.glyphicon-wrench + a(href="account/settings#password" data-i18n="general.password") Password + .panel.panel-default + .panel-heading + h3.panel-title + i.glyphicon.glyphicon-briefcase + a(href="account/settings#job-profile" data-i18n="account_settings.job_profile") Job Profile + .col-sm-6 + h2(data-i18n="user.recently_played") Recently Played + hr + if !recentlyPlayed + div(data-i18n="common.loading") Loading... + else if recentlyPlayed.length + table.table + tr + th(data-i18n="resources.level") Level + th(data-i18n="user.last_played") Last Played + th(data-i18n="user.status") Status + each session in recentlyPlayed + if session.get('levelName') + tr + td + - var posturl = '' + - if (session.get('team')) posturl = '?team=' + session.get('team') + a(href="/play/level/#{session.get('levelID') + posturl}")= session.get('levelName') + (session.get('team') ? ' (' + session.get('team') + ')' : '') + td= moment(session.get('changed')).fromNow() + if session.get('state').complete === true + td(data-i18n="user.status_completed") Completed + else if ! session.isMultiplayer() + td(data-i18n="user.status_unfinished") Unfinished + else + td + + else + .panel.panel-default + .panel-body + div(data-i18n="account.no_recent_games") No games played during the past two weeks. diff --git a/app/templates/achievement_notify.jade b/app/templates/achievement_notify.jade deleted file mode 100644 index f7ce0eb9b..000000000 --- a/app/templates/achievement_notify.jade +++ /dev/null @@ -1,12 +0,0 @@ -div - .clearfix.achievement-body - .achievement-image(data-notify-html="image") - .achievement-content - .achievement-title(data-notify-html="title") - .achievement-description(data-notify-html="description") - - .achievement-progress - .achievement-message(data-notify-html="message") - .progress-wrapper - .earned-exp(data-notify-html="earnedExp") - .progress-bar-wrapper(data-notify-html="progressBar") diff --git a/app/templates/achievements/achievement-popup.jade b/app/templates/achievements/achievement-popup.jade new file mode 100644 index 000000000..f3e53d4dc --- /dev/null +++ b/app/templates/achievements/achievement-popup.jade @@ -0,0 +1,21 @@ +- var addedClass = style + (locked === true ? ' locked' : '') +.clearfix.achievement-body(class=addedClass) + .achievement-icon + .achievement-image + img(src=imgURL) + .achievement-content + .achievement-title= title + p.achievement-description= description + + if popup + .progress-wrapper + span.user-level= level + .progress-bar-wrapper + .progress + - var currentTitle = $.i18n.t('achievements.current_xp_prefix') + currentXP + ' XP' + $.i18n.t('achievements.current_xp_postfix'); + - var newTitle = $.i18n.t('achievements.new_xp_prefix') + newXP + ' XP' + $.i18n.t('achievements.new_xp_postfix'); + - var leftTitle = $.i18n.t('achievements.left_xp_prefix') + newXP + ' XP' + $.i18n.t('achievements.left_xp_infix') + (level+1) + $.i18n.t('achievements.left_xp_postfix'); + .progress-bar.xp-bar-old(style="width:#{oldXPWidth}%" data-toggle="tooltip" data-placement="top" title="#{currentTitle}") + .progress-bar.xp-bar-new(style="width:#{newXPWidth}%" data-toggle="tooltip" title="#{newTitle}") + .progress-bar.xp-bar-left(style="width:#{leftXPWidth}%" data-toggle="tooltip" title="#{leftTitle}") + .progress-bar-border diff --git a/app/templates/base.jade b/app/templates/base.jade index d8bc8996f..a5f9c29bb 100644 --- a/app/templates/base.jade +++ b/app/templates/base.jade @@ -27,26 +27,44 @@ body select.language-dropdown - if me.get('anonymous') === false - button.btn.btn-primary.navbuttontext.header-font#logout-button(data-i18n="login.log_out") Log Out - a.btn.btn-primary.navbuttontext.header-font(href=me.get('jobProfile') ? "/account/profile/#{me.id}" : "/account/settings") - div.navbuttontext-account(data-i18n="nav.account") Account - if me.get('photoURL') - img.account-settings-image(src=me.getPhotoURL(18), alt="") - else - span.glyphicon.glyphicon-user - - else - button.btn.btn-primary.navbuttontext.header-font.auth-button - span(data-i18n="login.log_in") Log In - span.spr.spl / - span(data-i18n="login.sign_up") Create Account - ul(class='navbar-link-text').nav.navbar-nav.pull-right li.play a.header-font(href='/play', data-i18n="nav.play") Levels li a.header-font(href='/community', data-i18n="nav.community") Community + if me.get('anonymous') === false + li.dropdown + button.btn.btn-primary.navbuttontext.header-font.dropdown-toggle(href="#", data-toggle="dropdown") + if me.get('photoURL') + img.account-settings-image(src=me.getPhotoURL(18), alt="") + else + i.glyphicon.glyphicon-user + .navbuttontext-account(data-i18n="nav.account" href="/account") Account + span.caret + ul.dropdown-menu(role="menu") + li.user-dropdown-header + span.user-level= me.level() + a(href="/user/#{me.getSlugOrID()}") + img.img-circle(src="#{me.getPhotoURL()}" alt="") + h3=me.displayName() + li.user-dropdown-body + .col-xs-4.text-center + a(href="/user/#{me.getSlugOrID()}" data-i18n="nav.profile") Profile + .col-xs-4.text-center + a(href="/user/#{me.getSlugOrID()}/stats" data-i18n="nav.stats") Stats + .col-xs-4.text-center + a.disabled(data-i18n="nav.code") Code + li.user-dropdown-footer + .pull-left + a.btn.btn-default.btn-flat(href="/account" data-i18n="nav.account") Account + .pull-right + button#logout-button.btn.btn-default.btn-flat(data-i18n="login.log_out") Log Out + else + li + button.btn.btn-primary.navbuttontext.header-font.auth-button + span(data-i18n="login.log_in") Log In + span.spr.spl / + span(data-i18n="login.sign_up") Create Account block outer_content #outer-content-wrapper(class=showBackground ? 'show-background' : '') @@ -55,6 +73,7 @@ body .main-content-area block content p If this is showing, you dun goofed + .achievement-corner block footer .footer.clearfix diff --git a/app/templates/editor/achievement/edit.jade b/app/templates/editor/achievement/edit.jade index 42b59de17..17bab8045 100644 --- a/app/templates/editor/achievement/edit.jade +++ b/app/templates/editor/achievement/edit.jade @@ -2,16 +2,16 @@ extends /templates/base block content if me.isAdmin() - div - ol.breadcrumb - li - a(href="/editor", data-i18n="editor.main_title") CodeCombat Editors - li - a(href="/editor/achievement", data-i18n="editor.achievement_title") Achievement Editor - li.active - | #{achievement.attributes.name} + ol.breadcrumb + li + a(href="/editor", data-i18n="editor.main_title") CodeCombat Editors + li + a(href="/editor/achievement", data-i18n="editor.achievement_title") Achievement Editor + li.active + | #{achievement.attributes.name} button(data-i18n="", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#recalculate-button Recalculate + button(data-i18n="common.delete", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#delete-button Delete button(data-i18n="common.save", disabled=me.isAdmin() === true ? undefined : "true").btn.btn-primary#save-button Save h3(data-i18n="achievement.edit_achievement_title") Edit Achievement @@ -20,12 +20,10 @@ block content #achievement-treema - #achievement-view + #achievement-view.clearfix hr - div#error-view - else .alert.alert-danger span Admin only. Turn around. diff --git a/app/templates/editor/level/fork.jade b/app/templates/editor/fork-modal.jade similarity index 62% rename from app/templates/editor/level/fork.jade rename to app/templates/editor/fork-modal.jade index 6c4f43553..2b4db3463 100644 --- a/app/templates/editor/level/fork.jade +++ b/app/templates/editor/fork-modal.jade @@ -4,14 +4,14 @@ block modal-header-content h3(data-i18n="editor.fork_title") Fork New Version block modal-body-content - form#save-level-form.form + form.form .form-group - label(for="level-name", data-i18n="general.name") Name - input#level-name(name="name", type="text").form-control + label(for="model-name", data-i18n="general.name") Name + input#fork-model-name(name="name", type="text").form-control block modal-footer-content button.btn(data-dismiss="modal", data-i18n="common.cancel") Cancel - button.btn.btn-primary#fork-level-confirm-button(data-i18n="common.save") Save + button.btn.btn-primary#fork-model-confirm-button(data-i18n="common.save") Save block modal-body-wait-content h3(data-i18n="editor.fork_creating") Creating Fork... diff --git a/app/templates/editor/level/edit.jade b/app/templates/editor/level/edit.jade index 3f16f5559..d61965365 100644 --- a/app/templates/editor/level/edit.jade +++ b/app/templates/editor/level/edit.jade @@ -33,6 +33,8 @@ block header - var patches = level.get('patches') if patches && patches.length span.badge= patches.length + li + a(href="#related-achievements-view", data-toggle="tab") Achievements li a(href="#docs-components-view", data-toggle="tab", data-i18n="editor.level_tab_docs") Documentation .navbar-header @@ -83,7 +85,7 @@ block header span.spl(data-i18n="common.unwatch") Unwatch li(class=anonymous ? "disabled": "") - a(data-i18n="common.fork")#fork-level-start-button Fork + a(data-i18n="common.fork")#fork-start-button Fork li(class=anonymous ? "disabled": "") a(data-toggle="coco-modal", data-target="modal/RevertModal", data-i18n="editor.revert")#revert-button Revert li(class=anonymous ? "disabled": "") @@ -121,8 +123,10 @@ block outer_content div.tab-pane#editor-level-patches .patches-view + div.tab-pane#related-achievements-view + div.tab-pane#docs-components-view div#error-view -block footer \ No newline at end of file +block footer diff --git a/app/templates/editor/level/modal/new-achievement.jade b/app/templates/editor/level/modal/new-achievement.jade new file mode 100644 index 000000000..dfcc39cbc --- /dev/null +++ b/app/templates/editor/level/modal/new-achievement.jade @@ -0,0 +1,23 @@ +extends /templates/modal/new_model + +block modal-body-content + form.form + .form-group + label.control-label(for="name", data-i18n="general.name") Name + input#name.form-control(name="name", type="text") + .form-group + label.control-label(for="description" data-i18n="general.description") Description + input#description.form-control(name="description", type="text") + h4(data-i18n="editor.achievement_query_misc") Key achievement off of miscellanea + .radio + label + input(type="checkbox", name="queryOptions" id="misc-level-completion" value="misc-level-completion") + span.spl(data-i18n="editor.level_completion") Level Completion + - var goals = level.get('goals'); + if goals && goals.length + h4(data-i18n="editor.achievement_query_goals") Key achievement off of level goals + each goal in goals + .radio + label + input(type="checkbox", name="queryOptions" id="#{goal.id}" value="#{goal.id}") + span.spl= goal.name diff --git a/app/templates/editor/level/related-achievements.jade b/app/templates/editor/level/related-achievements.jade new file mode 100644 index 000000000..e0312d779 --- /dev/null +++ b/app/templates/editor/level/related-achievements.jade @@ -0,0 +1,26 @@ + +button.btn.btn-primary#new-achievement-button(disabled=me.isAdmin() === true ? undefined : "true" data-i18n="editor.new_achievement_title") Create a New Achievement + +if achievements.loading + h2(data-i18n="common.loading") Loading... +else if ! achievements.models.length + .panel + .panel-body + p(data-i18n="editor.no_achievements") No achievements added for this level yet. +else + table.table.table-hover + thead + tr + th + th(data-i18n="general.name") Name + th(data-i18n="general.description") Description + th XP + tbody + each achievement in achievements.models + tr + td(style="width: 20px") + img.achievement-icon-small(src=achievement.getImageURL() alt="#{achievement.get('name') icon") + td + a(href="/editor/achievement/#{achievement.get('slug')}")= achievement.get('name') + td= achievement.get('description') + td= achievement.get('worth') diff --git a/app/templates/editor/thang/thang-type-edit-view.jade b/app/templates/editor/thang/thang-type-edit-view.jade index f3560d212..394c1f97a 100644 --- a/app/templates/editor/thang/thang-type-edit-view.jade +++ b/app/templates/editor/thang/thang-type-edit-view.jade @@ -33,6 +33,16 @@ block header span.navbar-brand #{thangType.attributes.name} ul.nav.navbar-nav.navbar-right + li.dropdown + a(data-toggle='dropdown').play-with-level-parent + span.glyphicon-play.glyphicon + ul.dropdown-menu + li.dropdown-header Play Which Level? + li + for level in recentlyPlayedLevels + a.play-with-level-button(data-level=level)= level + input.play-with-level-input(placeholder="Type in a level name") + if authorized li#save-button a @@ -42,6 +52,8 @@ block header span.glyphicon-chevron-down.glyphicon ul.dropdown-menu li.dropdown-header Actions + li(class=anonymous ? "disabled": "") + a(data-i18n="common.fork")#fork-start-button Fork li(class=anonymous ? "disabled": "") a(data-toggle="coco-modal", data-target="modal/RevertModal", data-i18n="editor.revert")#revert-button Revert li.divider diff --git a/app/templates/game-menu/choose-hero-view.jade b/app/templates/game-menu/choose-hero-view.jade index 961ac33a2..acef1c498 100644 --- a/app/templates/game-menu/choose-hero-view.jade +++ b/app/templates/game-menu/choose-hero-view.jade @@ -2,10 +2,10 @@ h3(data-i18n="play_level.reload_title") Reload All Code? p(data-i18n="play_level.reload_really") Are you sure you want to reload this level back to the beginning? -if showDevBits - p - a(href='#', data-dismiss="modal", aria-hidden="true", data-i18n="play_level.reload_confirm").btn.btn-primary#restart-level-confirm-button Reload All +p + a(href='#', data-dismiss="modal", aria-hidden="true", data-i18n="play_level.reload_confirm").btn.btn-primary#restart-level-confirm-button Reload All +if showDevBits img(src="/images/pages/game-menu/choose-hero-stub.png") div(data-i18n="choose_hero.temp") Temp diff --git a/app/templates/game-menu/options-view.jade b/app/templates/game-menu/options-view.jade index 732392eda..ee4010dd2 100644 --- a/app/templates/game-menu/options-view.jade +++ b/app/templates/game-menu/options-view.jade @@ -8,6 +8,13 @@ .form h3(data-i18n="options.general_options") General Options + .form-group.slider-group + label(for="option-volume") + span(data-i18n="options.volume") Volume + span.spr : + span#option-volume-value= (me.get('volume') * 100).toFixed(0) + '%' + #option-volume.slider + .form-group.checkbox label(for="option-music") input#option-music(name="option-music", type="checkbox", checked=music) diff --git a/app/templates/kinds/search.jade b/app/templates/kinds/search.jade index 0e522d272..d69074636 100644 --- a/app/templates/kinds/search.jade +++ b/app/templates/kinds/search.jade @@ -12,32 +12,12 @@ block content if me.get('anonymous') a.btn.btn-primary.open-modal-button(data-toggle="coco-modal", data-target="modal/AuthModal", role="button", data-i18n="#{currentNewSignup}") Log in to Create a New Content else - a.btn.btn-primary.open-modal-button(href='#new-model-modal', role="button", data-toggle="modal", data-i18n="#{currentNew}") Create a New Something + a.btn.btn-primary.open-modal-button#new-model-button(data-i18n="#{currentNew}") Create a New Something input#search(data-i18n="[placeholder]#{currentSearch}") hr div.results table - // TODO: make this into a ModalView subview - div.modal.fade#new-model-modal - .modal-dialog - .background-wrapper - .modal-content - .modal-header - h3(data-i18n="#{currentNew}") Create New #{modelLabel} - .modal-body - form.form - .form-group - label.control-label(for="name", data-i18n="general.name") Name - input#name.form-control(name="name", type="text") - .modal-footer - button.btn(data-dismiss="modal", data-i18n="common.cancel") Cancel - button.btn.btn-primary.new-model-submit(data-i18n="common.create") Create - .modal-body.wait.secret - h3(data-i18n="play_level.tip_reticulating") Reticulating Splines... - .progress.progress-striped.active - .progress-bar - else .alert.alert-danger span Admin only. Turn around. diff --git a/app/templates/kinds/user.jade b/app/templates/kinds/user.jade new file mode 100644 index 000000000..1bcd19c1c --- /dev/null +++ b/app/templates/kinds/user.jade @@ -0,0 +1,13 @@ +extends /templates/base + +// User pages might have some user page specific header, if not remove this +block content + .clearfix + if user && viewName + ol.breadcrumb + li + a(href="/user/#{user.getSlugOrID()}") #{user.displayName()} + li.active + | #{viewName} + if !user || user.loading + | LOADING diff --git a/app/templates/modal/new_model.jade b/app/templates/modal/new_model.jade new file mode 100644 index 000000000..c904d1b96 --- /dev/null +++ b/app/templates/modal/new_model.jade @@ -0,0 +1,19 @@ +extends /templates/modal/modal_base + +block modal-header-content + h3(data-i18n="#{currentNew}") Create New #{modelLabel} + +block modal-body-content + form.form + .form-group + label.control-label(for="name", data-i18n="general.name") Name + input#name.form-control(name="name", type="text") + +block modal-footer + .modal-footer + button.btn(data-dismiss="modal", data-i18n="common.cancel") Cancel + button.btn.btn-primary.new-model-submit(data-i18n="common.create") Create + .modal-body.wait.secret + h3(data-i18n="play_level.tip_reticulating") Reticulating Splines... + .progress.progress-striped.active + .progress-bar diff --git a/app/templates/user/achievements.jade b/app/templates/user/achievements.jade new file mode 100644 index 000000000..33833d6f3 --- /dev/null +++ b/app/templates/user/achievements.jade @@ -0,0 +1,52 @@ +extends /templates/kinds/user + +block append content + .btn-group.pull-right + button#grid-layout-button.btn.btn-default(data-layout='grid', class=activeLayout==='grid' ? 'active' : '') + i.glyphicon.glyphicon-th + button#table-layout-button.btn.btn-default(data-layout='table', class=activeLayout==='table' ? 'active' : '') + i.glyphicon.glyphicon-th-list + if achievementsByCategory + if activeLayout === 'grid' + .grid-layout + each achievements, category in achievementsByCategory + .row + h2.achievement-category-title(data-i18n="category_#{category}")=category + each achievement, index in achievements + - var title = achievement.i18nName(); + - var description = achievement.i18nDescription(); + - var locked = ! achievement.get('unlocked'); + - var style = achievement.getStyle() + - var imgURL = achievement.getImageURL(); + if locked + - var imgURL = achievement.getLockedImageURL(); + else + - var imgURL = achievement.getImageURL(); + .col-lg-4.col-xs-12 + include ../achievements/achievement-popup + else if activeLayout === 'table' + .table-layout + if earnedAchievements.length + table.table + tr + th(data-i18n="general.name") Name + th(data-i18n="general.description") Description + th(data-i18n="general.date") Date + th(data-i18n="achievements.amount_achieved") Amount + th XP + each earnedAchievement in earnedAchievements.models + - var achievement = earnedAchievement.get('achievement'); + tr + td= achievement.i18nName() + td= achievement.i18nDescription() + td= moment().format("MMMM Do YYYY", earnedAchievement.get('changed')) + if achievement.isRepeatable() + td= earnedAchievement.get('achievedAmount') + else + td + td= earnedAchievement.get('earnedPoints') + else + .panel#no-achievements + .panel-body(data-i18n="user.no_achievements") No achievements earned yet. + else + div How did you even do that? diff --git a/app/templates/user/user_home.jade b/app/templates/user/user_home.jade new file mode 100644 index 000000000..7db5b13eb --- /dev/null +++ b/app/templates/user/user_home.jade @@ -0,0 +1,133 @@ +extends /templates/kinds/user + +block append content + if user + .vertical-buffer + .row + .left-column + .profile-wrapper + img.picture(src="#{user.getPhotoURL(150)}" alt="") + div.profile-info + h3.name= user.get('name') + if favoriteLanguage + div.extra-info + span(data-i18n="user.favorite_prefix") Favorite language is + strong.favorite-language= favoriteLanguage + span(data-i18n="user.favorite_postfix") . + .btn-group-vertical.profile-menu + a.btn.btn-default(href="/user/#{user.getSlugOrID()}/profile") + i.glyphicon.glyphicon-briefcase + span(data-i18n="account_settings.job_profile") Job Profile + a.btn.btn-default(href="/user/#{user.getSlugOrID()}/stats") + i.glyphicon.glyphicon-certificate + span(data-i18n="user.stats") Stats + a.btn.btn-default.disabled(href="#") + i.glyphicon.glyphicon-pencil + span(data-i18n="general.code") Code + - var emails = user.get('emails') + if emails + ul.contributor-categories + //li.contributor-category + img.contributor-image(src="/images/pages/user/general.png") + h4.contributor-title CodeCombateer + if emails.adventurerNews + li.contributor-category + img.contributor-image(src="/images/pages/user/adventurer.png") + h4.contributor-title + a(href="/contribute#adventurer" data-i18n="classes.adventurer_title") Adventurer + if emails.ambassadorNews + li.contributor-category + img.contributor-image(src="/images/pages/user/ambassador.png") + h4.contributor-title + a(href="/contribute#ambassador" data-i18n="classes.ambassador_title") Ambassador + if emails.archmageNews + li.contributor-category + img.contributor-image(src="/images/pages/user/archmage.png") + h4.contributor-title + a(href="/contribute#archmage" data-i18n="classes.archmage_title") Archmage + if emails.artisanNews + li.contributor-category + img.contributor-image(src="/images/pages/user/artisan.png") + h4.contributor-title + a(href="/contribute#artisan" data-i18n="classes.artisan_title") Artisan + if emails.scribeNews + li.contributor-category + img.contributor-image(src="/images/pages/user/scribe.png") + h4.contributor-title + a(href="/contribute#scribe" data-i18n="classes.scribe_title") Scribe + + .right-column + .panel.panel-default + .panel-heading + h3.panel-title(data-i18n="user.singleplayer_title") Singleplayer Levels + if (!singlePlayerSessions) + .panel-body + p(data-i18n="common.loading") Loading... + else if (singlePlayerSessions.length) + table.table + tr + th.col-xs-4(data-i18n="resources.level") Level + th.col-xs-4(data-i18n="user.last_played") Last Played + th.col-xs-4(data-i18n="user.status") Status + each session in singlePlayerSessions + if session.get('levelName') + tr + td + a(href="/play/level/#{session.get('levelID')}")= session.get('levelName') + td= moment(session.get('changed')).fromNow() + if session.get('state').complete === true + td(data-i18n="user.status_completed") Completed + else + td(data-i18n="user.status_unfinished") Unfinished + else + .panel-body + p(data-i18n="no_singleplayer") No Singleplayer games played yet. + .panel.panel-default + .panel-heading + h3.panel-title(data-i18n="no_multiplayer") Multiplayer Levels + if (!multiPlayerSessions) + .panel-body + p(data-i18n="common.loading") Loading... + else if (multiPlayerSessions.length) + table.table + tr + th.col-xs-4(data-i18n="resources.level") Level + th.col-xs-4(data-i18n="user.last_played") Last Played + th.col-xs-4(data-i18n="general.score") Score + each session in multiPlayerSessions + tr + td + - var posturl = '' + - if (session.get('team')) posturl = '?team=' + session.get('team') + a(href="/play/level/#{session.get('levelID') + posturl}")= session.get('levelName') + (session.get('team') ? ' (' + session.get('team') + ')' : '') + td= moment(session.get('changed')).fromNow() + if session.get('totalScore') + td= session.get('totalScore') * 100 + else + td(data-i18n="user.status_unfinished") Unfinished + else + .panel-body + p(data-i18n="user.no_multiplayer") No Multiplayer games played yet. + .panel.panel-default + .panel-heading + h3.panel-title(data-i18n="user.achievements") Achievements + if ! earnedAchievements + .panel-body + p(data-i18n="common.loading") Loading... + else if ! earnedAchievements.length + .panel-body + p(data-i18n="user.no_achievements") No achievements earned so far. + else + table.table + tr + th.col-xs-4(data-i18n="achievements.achievement") Achievement + th.col-xs-4(data-i18n="achievements.last_earned") Last Earned + th.col-xs-4(data-i18n="achievements.amount_achieved") Amount + each achievement in earnedAchievements.models + tr + td= achievement.get('achievementName') + td= moment().format("MMMM Do YYYY", achievement.get('changed')) + if achievement.get('achievedAmount') + td= achievement.get('achievedAmount') + else + td diff --git a/app/views/DemoView.coffee b/app/views/DemoView.coffee index 04cea9377..24fe9090d 100644 --- a/app/views/DemoView.coffee +++ b/app/views/DemoView.coffee @@ -1,4 +1,4 @@ -CocoView = require 'views/kinds/CocoView' +RootView = require 'views/kinds/RootView' ModalView = require 'views/kinds/ModalView' template = require 'templates/demo' requireUtils = require 'lib/requireUtils' @@ -24,7 +24,7 @@ DEMO_URL_PREFIX = '/demo/' ### -module.exports = DemoView = class DemoView extends CocoView +module.exports = DemoView = class DemoView extends RootView id: 'demo-view' template: template diff --git a/app/views/account/MainAccountView.coffee b/app/views/account/MainAccountView.coffee new file mode 100644 index 000000000..a14e2192a --- /dev/null +++ b/app/views/account/MainAccountView.coffee @@ -0,0 +1,38 @@ +View = require 'views/kinds/RootView' +template = require 'templates/account/account_home' +{me} = require 'lib/auth' +User = require 'models/User' +AuthModalView = require 'views/modal/AuthModal' +RecentlyPlayedCollection = require 'collections/RecentlyPlayedCollection' +ThangType = require 'models/ThangType' + +module.exports = class MainAccountView extends View + id: 'account-home' + template: template + + constructor: (options) -> + super options + return unless me + @wizardType = ThangType.loadUniversalWizard() + @recentlyPlayed = new RecentlyPlayedCollection me.get('_id') + @supermodel.loadModel @wizardType, 'thang' + @supermodel.loadCollection @recentlyPlayed, 'recentlyPlayed' + + onLoaded: -> + super() + + getRenderData: -> + c = super() + c.subs = {} + enabledEmails = c.me.getEnabledEmails() + c.subs[sub] = 1 for sub in enabledEmails + c.hasEmailNotes = _.any enabledEmails, (sub) -> sub.contains 'Notes' + c.hasEmailNews = _.any enabledEmails, (sub) -> sub.contains('News') and sub isnt 'generalNews' + c.hasGeneralNews = 'generalNews' in enabledEmails + c.wizardSource = @wizardType.getPortraitSource colorConfig: me.get('wizard')?.colorConfig if @wizardType.loaded + c.recentlyPlayed = @recentlyPlayed.models + c + + afterRender: -> + super() + @openModalView new AuthModalView if me.isAnonymous() diff --git a/app/views/achievements/AchievementPopup.coffee b/app/views/achievements/AchievementPopup.coffee new file mode 100644 index 000000000..82e464734 --- /dev/null +++ b/app/views/achievements/AchievementPopup.coffee @@ -0,0 +1,91 @@ +CocoView = require 'views/kinds/CocoView' +template = require 'templates/achievements/achievement-popup' +User = require '../../models/User' +Achievement = require '../../models/Achievement' + +module.exports = class AchievementPopup extends CocoView + className: 'achievement-popup' + template: template + + constructor: (options) -> + @achievement = options.achievement + @earnedAchievement = options.earnedAchievement + @container = options.container or @getContainer() + @popup = options.container + @popup ?= true + @className += ' popup' if @popup + super options + console.debug 'Created an AchievementPopup', @$el + + @render() + + calculateData: -> + currentLevel = me.level() + nextLevel = currentLevel + 1 + currentLevelExp = User.expForLevel(currentLevel) + nextLevelXP = User.expForLevel(nextLevel) + totalExpNeeded = nextLevelXP - currentLevelExp + expFunction = @achievement.getExpFunction() + currentXP = me.get 'points' + if @achievement.isRepeatable() + achievedXP = expFunction(@earnedAchievement.get('previouslyAchievedAmount')) * @achievement.get('worth') if @achievement.isRepeatable() + else + achievedXP = @achievement.get 'worth' + previousXP = currentXP - achievedXP + leveledUp = currentXP - achievedXP < currentLevelExp + #console.debug 'Leveled up' if leveledUp + alreadyAchievedPercentage = 100 * (previousXP - currentLevelExp) / totalExpNeeded + alreadyAchievedPercentage = 0 if alreadyAchievedPercentage < 0 # In case of level up + newlyAchievedPercentage = if leveledUp then 100 * (currentXP - currentLevelExp) / totalExpNeeded else 100 * achievedXP / totalExpNeeded + + #console.debug "Current level is #{currentLevel} (#{currentLevelExp} xp), next level is #{nextLevel} (#{nextLevelXP} xp)." + #console.debug "Need a total of #{nextLevelXP - currentLevelExp}, already had #{previousXP} and just now earned #{achievedXP} totalling on #{currentXP}" + + data = + title: @achievement.i18nName() + imgURL: @achievement.getImageURL() + description: @achievement.i18nDescription() + level: currentLevel + currentXP: currentXP + newXP: achievedXP + leftXP: nextLevelXP - currentXP + oldXPWidth: alreadyAchievedPercentage + newXPWidth: newlyAchievedPercentage + leftXPWidth: 100 - newlyAchievedPercentage - alreadyAchievedPercentage + + getRenderData: -> + c = super() + _.extend c, @calculateData() + c.style = @achievement.getStyle() + c.popup = true + c.$ = $ # Allows the jade template to do i18n + c + + render: -> + console.debug 'render achievement popup' + super() + @container.prepend @$el + if @popup + @$el.animate + left: 0 + @$el.on 'click', (e) => + @$el.animate + left: 600 + , => + @$el.remove() + @destroy() + + getContainer: -> + unless @container + @container = $('.achievement-popup-container') + unless @container.length + $('body').append('
') + @container = $('.achievement-popup-container') + @container + + afterRender: -> + super() + _.delay @initializeTooltips, 1000 # TODO this could be smoother + + initializeTooltips: -> + $('.progress-bar').tooltip() diff --git a/app/views/editor/ForkModal.coffee b/app/views/editor/ForkModal.coffee new file mode 100644 index 000000000..5acbaa1df --- /dev/null +++ b/app/views/editor/ForkModal.coffee @@ -0,0 +1,43 @@ +ModalView = require 'views/kinds/ModalView' +template = require 'templates/editor/fork-modal' +forms = require 'lib/forms' + +module.exports = class ForkModal extends ModalView + id: 'fork-modal' + template: template + instant: false + modalWidthPercent: 60 + + events: + 'click #fork-model-confirm-button': 'forkModel' + 'submit form': 'forkModel' + + constructor: (options) -> + super options + @editorPath = options.editorPath # like 'level' or 'thang' + @model = options.model + @modelClass = @model.constructor + + forkModel: -> + @showLoading() + forms.clearFormAlerts(@$el) + newModel = new @modelClass($.extend(true, {}, @model.attributes)) + newModel.unset '_id' + newModel.unset 'version' + newModel.unset 'creator' + newModel.unset 'created' + newModel.unset 'original' + newModel.unset 'parent' + newModel.set 'commitMessage', "Forked from #{@model.get('name')}" + newModel.set 'name', @$el.find('#fork-model-name').val() + if @model.get 'permissions' + newModel.set 'permissions', [access: 'owner', target: me.id] + newPathPrefix = "editor/#{@editorPath}/" + res = newModel.save() + return unless res + res.error => + @hideLoading() + forms.applyErrorsToForm(@$el.find('form'), JSON.parse(res.responseText)) + res.success => + @hide() + application.router.navigate(newPathPrefix + newModel.get('slug'), {trigger: true}) diff --git a/app/views/editor/achievement/AchievementEditView.coffee b/app/views/editor/achievement/AchievementEditView.coffee index ce5da02bb..98d6a4f32 100644 --- a/app/views/editor/achievement/AchievementEditView.coffee +++ b/app/views/editor/achievement/AchievementEditView.coffee @@ -1,7 +1,10 @@ RootView = require 'views/kinds/RootView' template = require 'templates/editor/achievement/edit' Achievement = require 'models/Achievement' +AchievementPopup = require 'views/achievements/AchievementPopup' ConfirmModal = require 'views/modal/ConfirmModal' +errors = require 'lib/errors' +app = require 'application' module.exports = class AchievementEditView extends RootView id: 'editor-achievement-edit-view' @@ -11,6 +14,7 @@ module.exports = class AchievementEditView extends RootView events: 'click #save-button': 'saveAchievement' 'click #recalculate-button': 'confirmRecalculation' + 'click #delete-button': 'confirmDeletion' subscriptions: 'save-new': 'saveAchievement' @@ -20,13 +24,10 @@ module.exports = class AchievementEditView extends RootView @achievement = new Achievement(_id: @achievementID) @achievement.saveBackups = true - @listenToOnce(@achievement, 'error', - () => - @hideLoading() - $(@$el).find('.main-content-area').children('*').not('#error-view').remove() - - @insertSubView(new ErrorView()) - ) + @achievement.once 'error', (achievement, jqxhr) => + @hideLoading() + $(@$el).find('.main-content-area').children('*').not('.breadcrumb').remove() + errors.backboneFailure arguments... @achievement.fetch() @listenToOnce(@achievement, 'sync', @buildTreema) @@ -49,17 +50,31 @@ module.exports = class AchievementEditView extends RootView @treema.build() - pushChangesToPreview: => - 'TODO' # TODO might want some intrinsic preview thing - getRenderData: (context={}) -> context = super(context) context.achievement = @achievement context.authorized = me.isAdmin() context + afterRender: -> + super(arguments...) + @pushChangesToPreview() + + pushChangesToPreview: => + $('#achievement-view').empty() + + if @treema? + for key, value of @treema.data + @achievement.set key, value + + earned = + earnedPoints: @achievement.get 'worth' + + popup = new AchievementPopup achievement: @achievement, earnedAchievement:earned, popup: false, container: $('#achievement-view') + + openSaveModal: -> - 'Maybe later' # TODO + 'Maybe later' # TODO patch patch patch saveAchievement: (e) -> @treema.endExistingEdits() @@ -75,20 +90,31 @@ module.exports = class AchievementEditView extends RootView url = "/editor/achievement/#{@achievement.get('slug') or @achievement.id}" document.location.href = url - confirmRecalculation: (e) -> + confirmRecalculation: -> renderData = 'confirmTitle': 'Are you really sure?' 'confirmBody': 'This will trigger recalculation of the achievement for all users. Are you really sure you want to go down this path?' 'confirmDecline': 'Not really' 'confirmConfirm': 'Definitely' - confirmModal = new ConfirmModal(renderData) - confirmModal.onConfirm @recalculateAchievement + confirmModal = new ConfirmModal renderData + confirmModal.on 'confirm', @recalculateAchievement + @openModalView confirmModal + + confirmDeletion: -> + renderData = + 'confirmTitle': 'Are you really sure?' + 'confirmBody': 'This will completely delete the achievement, potentially breaking a lot of stuff you don\'t want breaking. Are you entirely sure?' + 'confirmDecline': 'Not really' + 'confirmConfirm': 'Definitely' + + confirmModal = new ConfirmModal renderData + confirmModal.on 'confirm', @deleteAchievement @openModalView confirmModal recalculateAchievement: => $.ajax - data: JSON.stringify(achievements: [@achievement.get('slug') or @achievement.get('_id')]) + data: JSON.stringify(earnedAchievements: [@achievement.get('slug') or @achievement.get('_id')]) success: (data, status, jqXHR) -> noty timeout: 5000 @@ -105,3 +131,24 @@ module.exports = class AchievementEditView extends RootView url: '/admin/earned.achievement/recalculate' type: 'POST' contentType: 'application/json' + + deleteAchievement: => + console.debug 'deleting' + $.ajax + type: 'DELETE' + success: -> + noty + timeout: 5000 + text: 'Aaaand it\'s gone.' + type: 'success' + layout: 'topCenter' + _.delay -> + app.router.navigate '/editor/achievement', trigger: true + , 500 + error: (jqXHR, status, error) -> + console.error jqXHR + timeout: 5000 + text: "Deleting achievement failed with error code #{jqXHR.status}" + type: 'error' + layout: 'topCenter' + url: "/db/achievement/#{@achievement.id}" diff --git a/app/views/editor/level/LevelEditView.coffee b/app/views/editor/level/LevelEditView.coffee index 8e489291e..739144c5e 100644 --- a/app/views/editor/level/LevelEditView.coffee +++ b/app/views/editor/level/LevelEditView.coffee @@ -12,9 +12,10 @@ ScriptsTabView = require './scripts/ScriptsTabView' ComponentsTabView = require './components/ComponentsTabView' SystemsTabView = require './systems/SystemsTabView' SaveLevelModal = require './modals/SaveLevelModal' -LevelForkView = require './modals/ForkLevelModal' +ForkModal = require 'views/editor/ForkModal' SaveVersionModal = require 'views/modal/SaveVersionModal' PatchesView = require 'views/editor/PatchesView' +RelatedAchievementsView = require 'views/editor/level/RelatedAchievementsView' VersionHistoryView = require './modals/LevelVersionsModal' ComponentDocsView = require 'views/docs/ComponentDocumentationView' @@ -29,7 +30,7 @@ module.exports = class LevelEditView extends RootView 'click .play-with-team-button': 'onPlayLevel' 'click .play-with-team-parent': 'onPlayLevelTeamSelect' 'click #commit-level-start-button': 'startCommittingLevel' - 'click #fork-level-start-button': 'startForkingLevel' + 'click #fork-start-button': 'startForking' 'click #level-history-button': 'showVersionHistory' 'click #undo-button': 'onUndo' 'click #redo-button': 'onRedo' @@ -75,7 +76,8 @@ module.exports = class LevelEditView extends RootView @insertSubView new ScriptsTabView world: @world, supermodel: @supermodel, files: @files @insertSubView new ComponentsTabView supermodel: @supermodel @insertSubView new SystemsTabView supermodel: @supermodel - @insertSubView new ComponentDocsView() + @insertSubView new RelatedAchievementsView supermodel: @supermodel, level: @level + @insertSubView new ComponentDocsView supermodel: @supermodel Backbone.Mediator.publish 'level-loaded', level: @level @showReadOnly() if me.get('anonymous') @@ -128,9 +130,8 @@ module.exports = class LevelEditView extends RootView @openModalView new SaveLevelModal level: @level, supermodel: @supermodel Backbone.Mediator.publish 'level:view-switched', e - startForkingLevel: (e) -> - levelForkView = new LevelForkView level: @level - @openModalView levelForkView + startForking: (e) -> + @openModalView new ForkModal model: @level, editorPath: 'level' Backbone.Mediator.publish 'level:view-switched', e showVersionHistory: (e) -> diff --git a/app/views/editor/level/RelatedAchievementsView.coffee b/app/views/editor/level/RelatedAchievementsView.coffee new file mode 100644 index 000000000..e0ff97d54 --- /dev/null +++ b/app/views/editor/level/RelatedAchievementsView.coffee @@ -0,0 +1,40 @@ +CocoView = require 'views/kinds/CocoView' +template = require 'templates/editor/level/related-achievements' +RelatedAchievementsCollection = require 'collections/RelatedAchievementsCollection' +Achievement = require 'models/Achievement' +NewAchievementModal = require './modals/NewAchievementModal' +app = require 'application' + +module.exports = class RelatedAchievementsView extends CocoView + id: 'related-achievements-view' + template: template + className: 'tab-pane' + + events: + 'click #new-achievement-button': 'makeNewAchievement' + + constructor: (options) -> + super options + @level = options.level + @relatedID = @level.id + @achievements = new RelatedAchievementsCollection @relatedID + @supermodel.loadCollection @achievements, 'achievements' + + onLoaded: -> + console.debug 'related achievements loaded' + @achievements.loading = false + super() + + getRenderData: -> + c = super() + c.achievements = @achievements + c.relatedID = @relatedID + c + + onNewAchievementSaved: (achievement) -> + app.router.navigate('/editor/achievement/' + (achievement.get('slug') or achievement.id), {trigger: true}) + + makeNewAchievement: -> + modal = new NewAchievementModal model: Achievement, modelLabel: 'Achievement', level: @level + modal.once 'model-created', @onNewAchievementSaved + @openModalView modal diff --git a/app/views/editor/level/modals/ForkLevelModal.coffee b/app/views/editor/level/modals/ForkLevelModal.coffee deleted file mode 100644 index 335ffc4a1..000000000 --- a/app/views/editor/level/modals/ForkLevelModal.coffee +++ /dev/null @@ -1,45 +0,0 @@ -ModalView = require 'views/kinds/ModalView' -template = require 'templates/editor/level/fork' -forms = require 'lib/forms' -Level = require 'models/Level' - -module.exports = class ForkLevelModal extends ModalView - id: 'editor-level-fork-modal' - template: template - instant: false - modalWidthPercent: 60 - - events: - 'click #fork-level-confirm-button': 'forkLevel' - 'submit form': 'forkLevel' - - constructor: (options) -> - super options - @level = options.level - - getRenderData: (context={}) -> - context = super(context) - context.level = @level - context - - forkLevel: -> - @showLoading() - forms.clearFormAlerts(@$el) - newLevel = new Level($.extend(true, {}, @level.attributes)) - newLevel.unset '_id' - newLevel.unset 'version' - newLevel.unset 'creator' - newLevel.unset 'created' - newLevel.unset 'original' - newLevel.unset 'parent' - newLevel.set 'commitMessage', "Forked from #{@level.get('name')}" - newLevel.set 'name', @$el.find('#level-name').val() - newLevel.set 'permissions', [access: 'owner', target: me.id] - res = newLevel.save() - return unless res - res.error => - @hideLoading() - forms.applyErrorsToForm(@$el.find('form'), JSON.parse(res.responseText)) - res.success => - @hide() - application.router.navigate('editor/level/' + newLevel.get('slug'), {trigger: true}) diff --git a/app/views/editor/level/modals/NewAchievementModal.coffee b/app/views/editor/level/modals/NewAchievementModal.coffee new file mode 100644 index 000000000..a666bd239 --- /dev/null +++ b/app/views/editor/level/modals/NewAchievementModal.coffee @@ -0,0 +1,54 @@ +NewModelModal = require 'views/modal/NewModelModal' +template = require 'templates/editor/level/modal/new-achievement' +forms = require 'lib/forms' +Achievement = require 'models/Achievement' + +module.exports = class NewAchievementModal extends NewModelModal + id: 'new-achievement-modal' + template: template + plain: false + + constructor: (options) -> + super options + @level = options.level + + getRenderData: -> + c = super() + c.level = @level + console.debug 'level', c.level + c + + createQuery: -> + checked = @$el.find('[name=queryOptions]:checked') + checkedValues = ($(check).val() for check in checked) + subQueries = [] + for id in checkedValues + switch id + when 'misc-level-completion' + subQueries.push state: complete: true + else # It's a goal + q = state: goalStates: {} + q.state.goalStates[id] = {} + q.state.goalStates[id].status = 'success' + subQueries.push q + unless subQueries.length + query = {} + else if subQueries.length is 1 + query = subQueries[0] + else + query = $or: subQueries + query + + makeNewModel: -> + achievement = new Achievement + name = @$el.find('#name').val() + description = @$el.find('#description').val() + query = @createQuery() + + achievement.set 'name', name + achievement.set 'description', description + achievement.set 'query', query + achievement.set 'collection', 'level.sessions' + achievement.set 'userField', 'creator' + + achievement diff --git a/app/views/editor/thang/ThangTypeEditView.coffee b/app/views/editor/thang/ThangTypeEditView.coffee index 28e2aab39..4ddeadecb 100644 --- a/app/views/editor/thang/ThangTypeEditView.coffee +++ b/app/views/editor/thang/ThangTypeEditView.coffee @@ -10,8 +10,10 @@ ThangComponentsEditView = require 'views/editor/component/ThangComponentsEditVie ThangTypeVersionsModal = require './ThangTypeVersionsModal' ThangTypeColorsTabView = require './ThangTypeColorsTabView' PatchesView = require 'views/editor/PatchesView' +ForkModal = require 'views/editor/ForkModal' SaveVersionModal = require 'views/modal/SaveVersionModal' template = require 'templates/editor/thang/thang-type-edit-view' +storage = require 'lib/storage' CENTER = {x: 200, y: 300} @@ -35,8 +37,12 @@ module.exports = class ThangTypeEditView extends RootView 'click #marker-button': 'toggleDots' 'click #end-button': 'endAnimation' 'click #history-button': 'showVersionHistory' + 'click #fork-start-button': 'startForking' 'click #save-button': 'openSaveModal' 'click #patches-tab': -> @patchesView.load() + 'click .play-with-level-button': 'onPlayLevel' + 'click .play-with-level-parent': 'onPlayLevelSelect' + 'keyup .play-with-level-input': 'onPlayLevelKeyUp' subscriptions: 'save-new-version': 'saveNewThangType' @@ -58,6 +64,7 @@ module.exports = class ThangTypeEditView extends RootView context.thangType = @thangType context.animations = @getAnimationNames() context.authorized = not me.get('anonymous') + context.recentlyPlayedLevels = storage.load('recently-played-levels') ? ['items'] context getAnimationNames: -> @@ -401,12 +408,46 @@ module.exports = class ThangTypeEditView extends RootView @showingSelectedNode = false showVersionHistory: (e) -> - versionHistoryModal = new ThangTypeVersionsModal thangType: @thangType, @thangTypeID - @openModalView versionHistoryModal - Backbone.Mediator.publish 'level:view-switched', e + @openModalView new ThangTypeVersionsModal thangType: @thangType, @thangTypeID openSaveModal: -> - @openModalView(new SaveVersionModal({model: @thangType})) + @openModalView new SaveVersionModal model: @thangType + + startForking: (e) -> + @openModalView new ForkModal model: @thangType, editorPath: 'thang' + + onPlayLevelSelect: (e) -> + if @childWindow and not @childWindow.closed + # We already have a child window open, so we don't need to ask for a level; we'll use its existing level. + e.stopImmediatePropagation() + @onPlayLevel e + _.defer -> $('.play-with-level-input').focus() + + onPlayLevelKeyUp: (e) -> + return unless e.keyCode is 13 # return + input = @$el.find('.play-with-level-input') + input.parents('.dropdown').find('.play-with-level-parent').dropdown('toggle') + level = _.string.slugify input.val() + return unless level + @onPlayLevel null, level + recentlyPlayedLevels = storage.load('recently-played-levels') ? [] + recentlyPlayedLevels.push level + storage.save 'recently-played-levels', recentlyPlayedLevels + + onPlayLevel: (e, level=null) -> + level ?= $(e.target).data('level') + level = _.string.slugify level + if @childWindow and not @childWindow.closed + # Reset the LevelView's world, but leave the rest of the state alone + @childWindow.Backbone.Mediator.publish 'level-reload-thang-type', thangType: @thangType + else + # Create a new Window with a blank LevelView + scratchLevelID = level + '?dev=true' + if me.get('name') is 'Nick' + @childWindow = window.open("/play/level/#{scratchLevelID}", 'child_window', 'width=2560,height=1080,left=0,top=-1600,location=1,menubar=1,scrollbars=1,status=0,titlebar=1,toolbar=1', true) + else + @childWindow = window.open("/play/level/#{scratchLevelID}", 'child_window', 'width=1024,height=560,left=10,top=10,location=0,menubar=0,scrollbars=0,status=0,titlebar=0,toolbar=0', true) + @childWindow.focus() destroy: -> @camera?.destroy() diff --git a/app/views/game-menu/OptionsView.coffee b/app/views/game-menu/OptionsView.coffee index 8654554ef..e1bd2c53e 100644 --- a/app/views/game-menu/OptionsView.coffee +++ b/app/views/game-menu/OptionsView.coffee @@ -48,6 +48,20 @@ module.exports = class OptionsView extends CocoView afterRender: -> super() + @volumeSlider = @$el.find('#option-volume').slider(animate: 'fast', min: 0, max: 1, step: 0.05) + @volumeSlider.slider('value', me.get('volume')) + @volumeSlider.on('slide', @onVolumeSliderChange) + @volumeSlider.on('slidechange', @onVolumeSliderChange) + + destroy: -> + @volumeSlider?.slider?('destroy') + super() + + onVolumeSliderChange: (e) => + volume = @volumeSlider.slider('value') + me.set 'volume', volume + @$el.find('#option-volume-value').text (volume * 100).toFixed(0) + '%' + Backbone.Mediator.publish 'level-set-volume', volume: volume onHidden: -> if @playerName and @playerName isnt me.get('name') diff --git a/app/views/kinds/RootView.coffee b/app/views/kinds/RootView.coffee index f7bde789b..a5dac5e7e 100644 --- a/app/views/kinds/RootView.coffee +++ b/app/views/kinds/RootView.coffee @@ -6,8 +6,9 @@ CocoView = require './CocoView' {logoutUser, me} = require('lib/auth') locale = require 'locale/locale' -Achievement = require '../../models/Achievement' -User = require '../../models/User' +AchievementPopup = require 'views/achievements/AchievementPopup' +utils = require 'lib/utils' + # TODO remove filterKeyboardEvents = (allowedEvents, func) -> @@ -32,61 +33,13 @@ module.exports = class RootView extends CocoView 'achievements:new': 'handleNewAchievements' showNewAchievement: (achievement, earnedAchievement) -> - currentLevel = me.level() - nextLevel = currentLevel + 1 - currentLevelExp = User.expForLevel(currentLevel) - nextLevelExp = User.expForLevel(nextLevel) - totalExpNeeded = nextLevelExp - currentLevelExp - expFunction = achievement.getExpFunction() - currentExp = me.get('points') - previousExp = currentExp - achievement.get('worth') - previousExp = expFunction(earnedAchievement.get('previouslyAchievedAmount')) * achievement.get('worth') if achievement.isRepeatable() - achievedExp = currentExp - previousExp - leveledUp = currentExp - achievedExp < currentLevelExp - alreadyAchievedPercentage = 100 * (previousExp - currentLevelExp) / totalExpNeeded - newlyAchievedPercentage = if leveledUp then 100 * (currentExp - currentLevelExp) / totalExpNeeded else 100 * achievedExp / totalExpNeeded - - console.debug "Current level is #{currentLevel} (#{currentLevelExp} xp), next level is #{nextLevel} (#{nextLevelExp} xp)." - console.debug "Need a total of #{nextLevelExp - currentLevelExp}, already had #{previousExp} and just now earned #{achievedExp} totalling on #{currentExp}" - - alreadyAchievedBar = $("") - newlyAchievedBar = $("") - emptyBar = $("") - progressBar = $('').append(alreadyAchievedBar).append(newlyAchievedBar).append(emptyBar) - message = if (currentLevel isnt 1) and leveledUp then "Reached level #{currentLevel}!" else null - - alreadyAchievedBar.tooltip(title: "#{currentExp} XP in total") - newlyAchievedBar.tooltip(title: "#{achievedExp} XP earned") - emptyBar.tooltip(title: "#{nextLevelExp - currentExp} XP until level #{nextLevel}") - - # TODO a default should be linked here - imageURL = '/file/' + achievement.get('icon') - data = - title: achievement.get('name') - image: $("") - description: achievement.get('description') - progressBar: progressBar - earnedExp: "+ #{achievedExp} XP" - message: message - - options = - autoHideDelay: 10000 - globalPosition: 'bottom right' - showDuration: 400 - style: 'achievement' - autoHide: true - clickToHide: true - - $.notify( data, options ) + popup = new AchievementPopup achievement: achievement, earnedAchievement: earnedAchievement handleNewAchievements: (earnedAchievements) -> - _.each(earnedAchievements.models, (earnedAchievement) => + _.each earnedAchievements.models, (earnedAchievement) => achievement = new Achievement(_id: earnedAchievement.get('achievement')) - console.log achievement - achievement.fetch( + achievement.fetch success: (achievement) => @showNewAchievement(achievement, earnedAchievement) - ) - ) logoutAccount: -> logoutUser($('#login-email').val()) diff --git a/app/views/kinds/SearchView.coffee b/app/views/kinds/SearchView.coffee index 7fdd4cad6..1e41c8ead 100644 --- a/app/views/kinds/SearchView.coffee +++ b/app/views/kinds/SearchView.coffee @@ -1,6 +1,6 @@ RootView = require 'views/kinds/RootView' +NewModelModal = require 'views/modal/NewModelModal' template = require 'templates/kinds/search' -forms = require 'lib/forms' app = require 'application' class SearchCollection extends Backbone.Collection @@ -26,9 +26,7 @@ module.exports = class SearchView extends RootView events: 'change input#search': 'runSearch' 'keydown input#search': 'runSearch' - 'click button.new-model-submit': 'makeNewModel' - 'submit form': 'makeNewModel' - 'shown.bs.modal #new-model-modal': 'focusOnName' + 'click #new-model-button': 'newModel' 'hidden.bs.modal #new-model-modal': 'onModalHidden' constructor: (options) -> @@ -79,31 +77,11 @@ module.exports = class SearchView extends RootView @collection.off() @collection = null - makeNewModel: (e) -> - e.preventDefault() - name = @$el.find('#name').val() - model = new @model() - model.set('name', name) - if @model.schema.properties.permissions - model.set 'permissions', [{access: 'owner', target: me.id}] - res = model.save() - return unless res - - modal = @$el.find('#new-model-modal') - forms.clearFormAlerts(modal) - @showLoading(modal.find('.modal-body')) - res.error => - @hideLoading() - forms.applyErrorsToForm(modal, JSON.parse(res.responseText)) - that = @ - res.success -> - that.model = model - modal.modal('hide') - - onModalHidden: -> - # Can only redirect after the modal hidden event has triggered + onNewModelSaved: (@model) -> base = document.location.pathname[1..] + '/' app.router.navigate(base + (@model.get('slug') or @model.id), {trigger: true}) - focusOnName: -> - @$el.find('#name').focus() + newModel: (e) -> + modal = new NewModelModal model: @model, modelLabel: @modelLabel + modal.once 'success', @onNewModelSaved + @openModalView modal diff --git a/app/views/kinds/UserView.coffee b/app/views/kinds/UserView.coffee new file mode 100644 index 000000000..f2e997f1e --- /dev/null +++ b/app/views/kinds/UserView.coffee @@ -0,0 +1,35 @@ +RootView = require 'views/kinds/RootView' +template = require 'templates/kinds/user' +User = require 'models/User' + +module.exports = class UserView extends RootView + template: template + className: 'user-view' + viewName: null # Used for the breadcrumbs + + constructor: (@userID, options) -> + super options + @listenTo @, 'userNotFound', @ifUserNotFound + @fetchUser @userID + + fetchUser: (id) -> + if @isMe() + @user = me + @onLoaded() + @user = new User _id: id + @supermodel.loadModel @user, 'user' + + getRenderData: -> + context = super() + context.viewName = @viewName + context.user = @user unless @user?.isAnonymous() + context + + isMe: -> @userID is me.id + + onLoaded: -> + super() + + ifUserNotFound: -> + console.warn 'user not found' + @render() diff --git a/app/views/modal/ConfirmModal.coffee b/app/views/modal/ConfirmModal.coffee index 4749fc913..2ea188cf3 100644 --- a/app/views/modal/ConfirmModal.coffee +++ b/app/views/modal/ConfirmModal.coffee @@ -8,8 +8,8 @@ module.exports = class ConfirmModal extends ModalView closeOnConfirm: true events: - 'click #decline-button': 'doDecline' - 'click #confirm-button': 'doConfirm' + 'click #decline-button': 'onDecline' + 'click #confirm-button': 'onConfirm' constructor: (@renderData={}, options={}) -> super(options) @@ -21,10 +21,6 @@ module.exports = class ConfirmModal extends ModalView setRenderData: (@renderData) -> - onDecline: (@decline) -> + onDecline: -> @trigger 'decline' - onConfirm: (@confirm) -> - - doConfirm: -> @confirm() if @confirm - - doDecline: -> @decline() if @decline + onConfirm: -> @trigger 'confirm' diff --git a/app/views/modal/NewModelModal.coffee b/app/views/modal/NewModelModal.coffee new file mode 100644 index 000000000..0f7a33a10 --- /dev/null +++ b/app/views/modal/NewModelModal.coffee @@ -0,0 +1,54 @@ +ModalView = require 'views/kinds/ModalView' +template = require 'templates/modal/new_model' +forms = require 'lib/forms' + +module.exports = class NewModelModal extends ModalView + id: 'new-model-modal' + template: template + plain: false + + events: + 'click button.new-model-submit': 'onModelSubmitted' + 'submit form': 'onModelSubmitted' + + constructor: (options) -> + super options + @model = options.model + @modelLabel = options.modelLabel + @properties = options.properties + $('#name').ready @focusOnName + + getRenderData: -> + c = super() + c.modelLabel = @modelLabel + #c.newModelTitle = @newModelTitle + c + + makeNewModel: -> + model = new @model + name = @$el.find('#name').val() + model.set('name', name) + if @model.schema.properties.permissions + model.set 'permissions', [{access: 'owner', target: me.id}] + model.set(key, prop) for key, prop of @properties if @properties? + model + + onModelSubmitted: (e) -> + e.preventDefault() + model = @makeNewModel() + res = model.save() + return unless res + + forms.clearFormAlerts @$el + @showLoading(@$el.find('.modal-body')) + res.error => + @hideLoading() + forms.applyErrorsToForm(@$el, JSON.parse(res.responseText)) + #Backbone.Mediator.publish 'model-save-fail', model + res.success => + @$el.modal('hide') + @trigger 'model-created', model + #Backbone.Mediator.publish 'model-save-success', model + + focusOnName: (e) -> + $('#name').focus() # TODO Why isn't this working anymore.. It does get called diff --git a/app/views/play/MainPlayView.coffee b/app/views/play/MainPlayView.coffee index fa5aba4b6..c46f906c4 100644 --- a/app/views/play/MainPlayView.coffee +++ b/app/views/play/MainPlayView.coffee @@ -192,36 +192,8 @@ module.exports = class MainPlayView extends RootView levelPath: 'ladder' } ] - - playerCreated = [ - { - name: 'Extra Extrapolation' - difficulty: 2 - id: 'extra-extrapolation' - image: '/file/db/level/526bda3fe79aefde2a003e36/mobile_artillery_icon.png' - description: 'Predict your target\'s position for deadly aim. - by Sootn' - } - { - name: 'The Right Route' - difficulty: 1 - id: 'the-right-route' - image: '/file/db/level/526fd3043c637ece50001bb2/the_herd_icon.png' - description: 'Strike at the weak point in an array of enemies. - by Aftermath' - } - { - name: 'Sword Loop' - difficulty: 2 - id: 'sword-loop' - image: '/file/db/level/525dc5589a0765e496000006/drink_me_icon.png' - description: 'Kill the ogres and save the peasants with for-loops. - by Prabh Simran Singh Baweja' - } - { - name: 'Coin Mania' - difficulty: 2 - id: 'coin-mania' - image: '/file/db/level/529662dfe0df8f0000000007/grab_the_mushroom_icon.png' - description: 'Learn while-loops to grab coins and potions. - by Prabh Simran Singh Baweja' - } + + classicAlgorithms = [ { name: 'Bubble Sort Bootcamp Battle' difficulty: 3 @@ -257,6 +229,37 @@ module.exports = class MainPlayView extends RootView image: '/file/db/level/525ef8ef06e1ab0962000003/commanding_followers_icon.png' description: 'Learn Quicksort while sorting a spiral of ogres! - by Alexandru Caciulescu' } + ] + + playerCreated = [ + { + name: 'Extra Extrapolation' + difficulty: 2 + id: 'extra-extrapolation' + image: '/file/db/level/526bda3fe79aefde2a003e36/mobile_artillery_icon.png' + description: 'Predict your target\'s position for deadly aim. - by Sootn' + } + { + name: 'The Right Route' + difficulty: 1 + id: 'the-right-route' + image: '/file/db/level/526fd3043c637ece50001bb2/the_herd_icon.png' + description: 'Strike at the weak point in an array of enemies. - by Aftermath' + } + { + name: 'Sword Loop' + difficulty: 2 + id: 'sword-loop' + image: '/file/db/level/525dc5589a0765e496000006/drink_me_icon.png' + description: 'Kill the ogres and save the peasants with for-loops. - by Prabh Simran Singh Baweja' + } + { + name: 'Coin Mania' + difficulty: 2 + id: 'coin-mania' + image: '/file/db/level/529662dfe0df8f0000000007/grab_the_mushroom_icon.png' + description: 'Learn while-loops to grab coins and potions. - by Prabh Simran Singh Baweja' + } { name: 'Find the Spy' difficulty: 2 @@ -291,6 +294,7 @@ module.exports = class MainPlayView extends RootView {id: 'beginner', name: 'Beginner Campaign', description: '... in which you learn the wizardry of programming.', levels: tutorials} {id: 'multiplayer', name: 'Multiplayer Arenas', description: '... in which you code head-to-head against other players.', levels: arenas} {id: 'dev', name: 'Random Harder Levels', description: '... in which you learn the interface while doing something a little harder.', levels: experienced} + {id: 'classic' ,name: 'Classic Algorithms', description: '... in which you learn the most popular algorithms in Computer Science.', levels: classicAlgorithms} {id: 'player_created', name: 'Player-Created', description: '... in which you battle against the creativity of your fellow Artisan Wizards.', levels: playerCreated} ] context.levelStatusMap = @levelStatusMap diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee index 663dbd70f..391c99f4c 100644 --- a/app/views/play/level/PlayLevelView.coffee +++ b/app/views/play/level/PlayLevelView.coffee @@ -55,6 +55,7 @@ module.exports = class PlayLevelView extends RootView 'god:new-world-created': 'onNewWorld' 'god:infinite-loop': 'onInfiniteLoop' 'level-reload-from-data': 'onLevelReloadFromData' + 'level-reload-thang-type': 'onLevelReloadThangType' 'play-next-level': 'onPlayNextLevel' 'edit-wizard-settings': 'showWizardSettingsModal' 'surface:world-set-up': 'onSurfaceSetUpNewWorld' @@ -326,6 +327,15 @@ module.exports = class PlayLevelView extends RootView @scriptManager.setScripts(e.level.get('scripts')) Backbone.Mediator.publish 'tome:cast-spell' # a bit hacky + onLevelReloadThangType: (e) -> + tt = e.thangType + for url, model of @supermodel.models + if model.id is tt.id + for key, val of tt.attributes + model.attributes[key] = val + break + Backbone.Mediator.publish 'tome:cast-spell' + onWindowResize: (s...) -> $('#pointer').css('opacity', 0.0) diff --git a/app/views/user/AchievementsView.coffee b/app/views/user/AchievementsView.coffee new file mode 100644 index 000000000..c5a7f32bb --- /dev/null +++ b/app/views/user/AchievementsView.coffee @@ -0,0 +1,56 @@ +UserView = require 'views/kinds/UserView' +template = require 'templates/user/achievements' +{me} = require 'lib/auth' +Achievement = require 'models/Achievement' +EarnedAchievement = require 'models/EarnedAchievement' +AchievementCollection = require 'collections/AchievementCollection' +EarnedAchievementCollection = require 'collections/EarnedAchievementCollection' + +module.exports = class AchievementsView extends UserView + id: 'user-achievements-view' + template: template + viewName: 'Stats' + activeLayout: 'grid' + + events: + 'click #grid-layout-button': 'layoutChanged' + 'click #table-layout-button': 'layoutChanged' + + constructor: (userID, options) -> + super options, userID + + onLoaded: -> + unless @achievements or @earnedAchievements + @supermodel.resetProgress() + @achievements = new AchievementCollection + @earnedAchievements = new EarnedAchievementCollection @user.getSlugOrID() + @supermodel.loadCollection @achievements, 'achievements' + @supermodel.loadCollection @earnedAchievements, 'earnedAchievements' + else + for earned in @earnedAchievements.models + return unless relatedAchievement = _.find @achievements.models, (achievement) -> + achievement.get('_id') is earned.get 'achievement' + relatedAchievement.set 'unlocked', true + earned.set 'achievement', relatedAchievement + deferredImages = (achievement.cacheLockedImage() for achievement in @achievements.models when not achievement.get 'unlocked') + whenever = $.when deferredImages... + whenever.done => @render() + super() + + layoutChanged: (e) -> + @activeLayout = $(e.currentTarget).data 'layout' + @render() + + getRenderData: -> + context = super() + context.activeLayout = @activeLayout + + # After user is loaded + if @user and not @user.isAnonymous() + context.earnedAchievements = @earnedAchievements + context.achievements = @achievements + context.achievementsByCategory = {} + for achievement in @achievements.models + context.achievementsByCategory[achievement.get('category')] ?= [] + context.achievementsByCategory[achievement.get('category')].push achievement + context diff --git a/app/views/account/JobProfileCodeModal.coffee b/app/views/user/JobProfileCodeModal.coffee similarity index 100% rename from app/views/account/JobProfileCodeModal.coffee rename to app/views/user/JobProfileCodeModal.coffee diff --git a/app/views/account/JobProfileView.coffee b/app/views/user/JobProfileView.coffee similarity index 96% rename from app/views/account/JobProfileView.coffee rename to app/views/user/JobProfileView.coffee index 6b2ed8bfd..47451d590 100644 --- a/app/views/account/JobProfileView.coffee +++ b/app/views/user/JobProfileView.coffee @@ -1,4 +1,4 @@ -RootView = require 'views/kinds/RootView' +UserView = require 'views/kinds/UserView' template = require 'templates/account/profile' User = require 'models/User' LevelSession = require 'models/LevelSession' @@ -26,7 +26,7 @@ adminContacts = [ {id: '52a57252a89409700d0000d9', name: 'Ignore'} ] -module.exports = class JobProfileView extends RootView +module.exports = class JobProfileView extends UserView id: 'profile-view' template: template showBackground: false @@ -54,8 +54,7 @@ module.exports = class JobProfileView extends RootView 'change #admin-contact': 'onAdminContactChanged' 'click .session-link': 'onSessionLinkPressed' - constructor: (options, @userID) -> - @userID ?= me.id + constructor: (userID, options) -> @onJobProfileNotesChanged = _.debounce @onJobProfileNotesChanged, 1000 @onRemarkChanged = _.debounce @onRemarkChanged, 1000 @authorizedWithLinkedIn = IN?.User?.isAuthorized() @@ -64,32 +63,19 @@ module.exports = class JobProfileView extends RootView window.contractCallback = => @authorizedWithLinkedIn = IN?.User?.isAuthorized() @render() - super options - if me.get('anonymous') is true - @render() - return - if User.isObjectID @userID - @finishInit() - else - $.ajax "/db/user/#{@userID}/nameToID", success: (@userID) => - @finishInit() unless @destroyed - @render() + super options, userID + + onUserLoaded: -> + @finishInit() unless @destroyed + super() finishInit: -> return unless @userID @uploadFilePath = "db/user/#{@userID}" @highlightedContainers = [] - if @userID is me.id - @user = me - else if me.isAdmin() or 'employer' in me.get('permissions') - @user = User.getByID(@userID) - @user.fetch() - @listenTo @user, 'sync', => - @render() + if me.isAdmin() or 'employer' in me.get('permissions') $.post "/db/user/#{me.id}/track/view_candidate" $.post "/db/user/#{@userID}/track/viewed_by_employer" unless me.isAdmin() - else - @user = User.getByID(@userID) @sessions = @supermodel.loadCollection(new LevelSessionsCollection(@userID), 'candidate_sessions').model if me.isAdmin() # Mimicking how the VictoryModal fetches LevelFeedback @@ -248,7 +234,7 @@ module.exports = class JobProfileView extends RootView jobProfile.name ?= (@user.get('firstName') + ' ' + @user.get('lastName')).trim() if @user?.get('firstName') context.profile = jobProfile context.user = @user - context.myProfile = @user?.id is context.me.id + context.myProfile = @isMe() context.allowedToViewJobProfile = @user and (me.isAdmin() or 'employer' in me.get('permissions') or (context.myProfile && !me.get('anonymous'))) context.allowedToEditJobProfile = @user and (me.isAdmin() or (context.myProfile && !me.get('anonymous'))) context.profileApproved = @user?.get 'jobProfileApproved' @@ -289,7 +275,7 @@ module.exports = class JobProfileView extends RootView _.delay -> justSavedSection.removeClass 'just-saved', duration: 1500, easing: 'easeOutQuad' , 500 - if me.isAdmin() + if me.isAdmin() and @user visibleSettings = ['history', 'tasks'] data = _.pick (@remark.attributes), (value, key) -> key in visibleSettings data.history ?= [] @@ -596,4 +582,4 @@ module.exports = class JobProfileView extends RootView sessionID = $(e.target).closest('.session-link').data('session-id') session = _.find @sessions.models, (session) -> session.id is sessionID modal = new JobProfileCodeModal({session:session}) - @openModalView modal \ No newline at end of file + @openModalView modal diff --git a/app/views/user/MainUserView.coffee b/app/views/user/MainUserView.coffee new file mode 100644 index 000000000..a200ab989 --- /dev/null +++ b/app/views/user/MainUserView.coffee @@ -0,0 +1,54 @@ +UserView = require 'views/kinds/UserView' +CocoCollection = require 'collections/CocoCollection' +LevelSession = require 'models/LevelSession' +template = require 'templates/user/user_home' +{me} = require 'lib/auth' +EarnedAchievementCollection = require 'collections/EarnedAchievementCollection' + +class LevelSessionsCollection extends CocoCollection + model: LevelSession + + constructor: (userID) -> + @url = "/db/user/#{userID}/level.sessions?project=state.complete,levelID,levelName,changed,team,submittedCodeLanguage,totalScore&order=-1" + super() + +module.exports = class MainUserView extends UserView + id: 'user-home' + template: template + + constructor: (userID, options) -> + super options + + getRenderData: -> + context = super() + if @levelSessions and @levelSessions.loaded + singlePlayerSessions = [] + multiPlayerSessions = [] + languageCounts = {} + for levelSession in @levelSessions.models + if levelSession.isMultiplayer() + multiPlayerSessions.push levelSession + else + singlePlayerSessions.push levelSession + languageCounts[levelSession.get 'submittedCodeLanguage'] = (languageCounts[levelSession.get 'submittedCodeLanguage'] or 0) + 1 + mostUsedCount = 0 + favoriteLanguage = null + for language, count of languageCounts + if count > mostUsedCount + mostUsedCount = count + favoriteLanguage = language + context.singlePlayerSessions = singlePlayerSessions + context.multiPlayerSessions = multiPlayerSessions + context.favoriteLanguage = favoriteLanguage + if @earnedAchievements and @earnedAchievements.loaded + context.earnedAchievements = @earnedAchievements + context + + onLoaded: -> + if @user.loaded and not (@earnedAchievements or @levelSessions) + @supermodel.resetProgress() + @levelSessions = new LevelSessionsCollection @user.getSlugOrID() + @earnedAchievements = new EarnedAchievementCollection @user.getSlugOrID() + @supermodel.loadCollection @levelSessions, 'levelSessions' + @supermodel.loadCollection @earnedAchievements, 'earnedAchievements' + super() diff --git a/bower.json b/bower.json index a909afb13..69204f667 100644 --- a/bower.json +++ b/bower.json @@ -32,7 +32,7 @@ "firepad": "~0.1.2", "marked": "~0.3.0", "moment": "~2.5.0", - "aether": "~0.2.22", + "aether": "~0.2.28", "underscore.string": "~2.3.3", "firebase": "~1.0.2", "catiline": "~2.9.3", @@ -40,7 +40,7 @@ "jsondiffpatch": "~0.1.5", "nanoscroller": "~0.8.0", "jquery.tablesorter": "~2.15.13", - "treema": "~0.0.12", + "treema": "~0.0.14", "bootstrap": "~3.1.1", "validated-backbone-mediator": "~0.1.3", "jquery.browser": "~0.0.6", diff --git a/package.json b/package.json index 27d76ce3b..7bf7b7893 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "winston": "0.6.x", "passport": "0.1.x", "passport-local": "0.1.x", + "moment": "~2.5.0", "mongodb": "1.2.x", "mongoose": "3.8.x", "mongoose-text-search": "~0.0.2", @@ -65,7 +66,7 @@ "redis": "", "webworker-threads": "~0.4.11", "node-gyp": "~0.13.0", - "aether": "~0.2.22", + "aether": "~0.2.28", "JASON": "~0.1.3", "JQDeferred": "~2.1.0" }, diff --git a/public/images/achievements/achievement_background.png b/public/images/achievements/achievement_background.png new file mode 100644 index 000000000..c74ff1bf2 Binary files /dev/null and b/public/images/achievements/achievement_background.png differ diff --git a/public/images/achievements/achievement_background_light.png b/public/images/achievements/achievement_background_light.png new file mode 100644 index 000000000..53e09e62d Binary files /dev/null and b/public/images/achievements/achievement_background_light.png differ diff --git a/public/images/achievements/achievement_background_locked.png b/public/images/achievements/achievement_background_locked.png new file mode 100644 index 000000000..94a300b12 Binary files /dev/null and b/public/images/achievements/achievement_background_locked.png differ diff --git a/public/images/achievements/bar_border.png b/public/images/achievements/bar_border.png new file mode 100644 index 000000000..9ad03ef20 Binary files /dev/null and b/public/images/achievements/bar_border.png differ diff --git a/public/images/achievements/border_diamond.png b/public/images/achievements/border_diamond.png new file mode 100644 index 000000000..a2e10f8f0 Binary files /dev/null and b/public/images/achievements/border_diamond.png differ diff --git a/public/images/achievements/border_diamond_locked.png b/public/images/achievements/border_diamond_locked.png new file mode 100644 index 000000000..bc56fbc75 Binary files /dev/null and b/public/images/achievements/border_diamond_locked.png differ diff --git a/public/images/achievements/border_gold.png b/public/images/achievements/border_gold.png new file mode 100644 index 000000000..7a46157c1 Binary files /dev/null and b/public/images/achievements/border_gold.png differ diff --git a/public/images/achievements/border_gold_locked.png b/public/images/achievements/border_gold_locked.png new file mode 100644 index 000000000..f1ee95e3d Binary files /dev/null and b/public/images/achievements/border_gold_locked.png differ diff --git a/public/images/achievements/border_silver.png b/public/images/achievements/border_silver.png new file mode 100644 index 000000000..016d48b00 Binary files /dev/null and b/public/images/achievements/border_silver.png differ diff --git a/public/images/achievements/border_silver_locked.png b/public/images/achievements/border_silver_locked.png new file mode 100644 index 000000000..44bcc7e84 Binary files /dev/null and b/public/images/achievements/border_silver_locked.png differ diff --git a/public/images/achievements/border_stone.png b/public/images/achievements/border_stone.png new file mode 100644 index 000000000..7d13718ce Binary files /dev/null and b/public/images/achievements/border_stone.png differ diff --git a/public/images/achievements/border_stone_locked.png b/public/images/achievements/border_stone_locked.png new file mode 100644 index 000000000..66f92b2b4 Binary files /dev/null and b/public/images/achievements/border_stone_locked.png differ diff --git a/public/images/achievements/border_wood.png b/public/images/achievements/border_wood.png new file mode 100644 index 000000000..368878c20 Binary files /dev/null and b/public/images/achievements/border_wood.png differ diff --git a/public/images/achievements/border_wood_locked.png b/public/images/achievements/border_wood_locked.png new file mode 100644 index 000000000..167e87421 Binary files /dev/null and b/public/images/achievements/border_wood_locked.png differ diff --git a/public/images/achievements/cross-01.png b/public/images/achievements/cross-01.png new file mode 100644 index 000000000..6a2ab02ef Binary files /dev/null and b/public/images/achievements/cross-01.png differ diff --git a/public/images/achievements/cup-01.png b/public/images/achievements/cup-01.png new file mode 100644 index 000000000..3b4c4c40b Binary files /dev/null and b/public/images/achievements/cup-01.png differ diff --git a/public/images/achievements/cup-02.png b/public/images/achievements/cup-02.png new file mode 100644 index 000000000..0eba69463 Binary files /dev/null and b/public/images/achievements/cup-02.png differ diff --git a/public/images/achievements/default.png b/public/images/achievements/default.png new file mode 100644 index 000000000..adc2d5be4 Binary files /dev/null and b/public/images/achievements/default.png differ diff --git a/public/images/achievements/level-bg.png b/public/images/achievements/level-bg.png new file mode 100644 index 000000000..9a23c0a6b Binary files /dev/null and b/public/images/achievements/level-bg.png differ diff --git a/public/images/achievements/message-01.png b/public/images/achievements/message-01.png new file mode 100644 index 000000000..8fe3dac44 Binary files /dev/null and b/public/images/achievements/message-01.png differ diff --git a/public/images/achievements/patch-01.png b/public/images/achievements/patch-01.png new file mode 100644 index 000000000..dba898f40 Binary files /dev/null and b/public/images/achievements/patch-01.png differ diff --git a/public/images/achievements/pendant-01.png b/public/images/achievements/pendant-01.png new file mode 100644 index 000000000..0d0b8fc00 Binary files /dev/null and b/public/images/achievements/pendant-01.png differ diff --git a/public/images/achievements/scroll-01.png b/public/images/achievements/scroll-01.png new file mode 100644 index 000000000..a7ab56721 Binary files /dev/null and b/public/images/achievements/scroll-01.png differ diff --git a/public/images/achievements/star.png b/public/images/achievements/star.png new file mode 100644 index 000000000..22f86961d Binary files /dev/null and b/public/images/achievements/star.png differ diff --git a/public/images/achievements/swords-01.png b/public/images/achievements/swords-01.png new file mode 100644 index 000000000..3d7b5170d Binary files /dev/null and b/public/images/achievements/swords-01.png differ diff --git a/public/images/pages/user/adventurer.png b/public/images/pages/user/adventurer.png new file mode 100644 index 000000000..2729d87b8 Binary files /dev/null and b/public/images/pages/user/adventurer.png differ diff --git a/public/images/pages/user/ambassador.png b/public/images/pages/user/ambassador.png new file mode 100644 index 000000000..f30002b33 Binary files /dev/null and b/public/images/pages/user/ambassador.png differ diff --git a/public/images/pages/user/archmage.png b/public/images/pages/user/archmage.png new file mode 100644 index 000000000..dad621ee0 Binary files /dev/null and b/public/images/pages/user/archmage.png differ diff --git a/public/images/pages/user/artisan.png b/public/images/pages/user/artisan.png new file mode 100644 index 000000000..351b1eace Binary files /dev/null and b/public/images/pages/user/artisan.png differ diff --git a/public/images/pages/user/diplomat.png b/public/images/pages/user/diplomat.png new file mode 100644 index 000000000..76c1cb6a7 Binary files /dev/null and b/public/images/pages/user/diplomat.png differ diff --git a/public/images/pages/user/general.png b/public/images/pages/user/general.png new file mode 100644 index 000000000..f57cf15d6 Binary files /dev/null and b/public/images/pages/user/general.png differ diff --git a/public/images/pages/user/scribe.png b/public/images/pages/user/scribe.png new file mode 100644 index 000000000..8c19f22ba Binary files /dev/null and b/public/images/pages/user/scribe.png differ diff --git a/scripts/recalculateAchievements.coffee b/scripts/recalculateAchievements.coffee new file mode 100644 index 000000000..2ec5e02c0 --- /dev/null +++ b/scripts/recalculateAchievements.coffee @@ -0,0 +1,19 @@ +database = require '../server/commons/database' +mongoose = require 'mongoose' +log = require 'winston' +async = require 'async' + +### SET UP ### +do (setupLodash = this) -> + GLOBAL._ = require 'lodash' + _.str = require 'underscore.string' + _.mixin _.str.exports() + +database.connect() + +EarnedAchievementHandler = require '../server/achievements/earned_achievement_handler' +log.info 'Starting earned achievement recalculation...' +EarnedAchievementHandler.constructor.recalculate (err) -> + log.error err if err? + log.info 'Finished recalculating all earned achievements.' + process.exit() diff --git a/scripts/recalculateStatistics.coffee b/scripts/recalculateStatistics.coffee new file mode 100644 index 000000000..7177acf9d --- /dev/null +++ b/scripts/recalculateStatistics.coffee @@ -0,0 +1,54 @@ +database = require '../server/commons/database' +mongoose = require 'mongoose' +log = require 'winston' +async = require 'async' + +### SET UP ### +do (setupLodash = this) -> + GLOBAL._ = require 'lodash' + _.str = require 'underscore.string' + _.mixin _.str.exports() + +database.connect() + +### USER STATS ### +UserHandler = require '../server/users/user_handler' + +report = (func, name, done) -> + log.info 'Started ' + name + '...' + func name, (err) -> + log.warn err if err? + log.info 'Finished ' + name + done err if done? + +whenAllFinished = -> + log.info 'All recalculations finished.' + process.exit() + +async.parallel [ + # Misc + (c) -> report UserHandler.recalculateStats, 'gamesCompleted', c + # Edits + (c) -> report UserHandler.recalculateStats, 'articleEdits', c + (c) -> report UserHandler.recalculateStats, 'levelEdits', c + (c) -> report UserHandler.recalculateStats, 'levelComponentEdits', c + (c) -> report UserHandler.recalculateStats, 'levelSystemEdits', c + (c) -> report UserHandler.recalculateStats, 'thangTypeEdits', c + # Patches + (c) -> report UserHandler.recalculateStats, 'patchesContributed', c + (c) -> report UserHandler.recalculateStats, 'patchesSubmitted', c + (c) -> report UserHandler.recalculateStats, 'totalTranslationPatches', c + (c) -> report UserHandler.recalculateStats, 'totalMiscPatches', c + + (c) -> report UserHandler.recalculateStats, 'articleMiscPatches', c + (c) -> report UserHandler.recalculateStats, 'levelMiscPatches', c + (c) -> report UserHandler.recalculateStats, 'levelComponentMiscPatches', c + (c) -> report UserHandler.recalculateStats, 'levelSystemMiscPatches', c + (c) -> report UserHandler.recalculateStats, 'thangTypeMiscPatches', c + + (c) -> report UserHandler.recalculateStats, 'articleTranslationPatches', c + (c) -> report UserHandler.recalculateStats, 'levelTranslationPatches', c + (c) -> report UserHandler.recalculateStats, 'levelComponentTranslationPatches', c + (c) -> report UserHandler.recalculateStats, 'levelSystemTranslationPatches', c + (c) -> report UserHandler.recalculateStats, 'thangTypeTranslationPatches', c +], whenAllFinished diff --git a/scripts/setupAchievements.coffee b/scripts/setupAchievements.coffee new file mode 100644 index 000000000..42cfb4e9f --- /dev/null +++ b/scripts/setupAchievements.coffee @@ -0,0 +1,232 @@ +database = require '../server/commons/database' +mongoose = require 'mongoose' +log = require 'winston' +async = require 'async' + +### SET UP ### +do (setupLodash = this) -> + GLOBAL._ = require 'lodash' + _.str = require 'underscore.string' + _.mixin _.str.exports() + +database.connect() + + +## Util + +## Types +contributor = (obj) -> + _.extend obj, # This way we get the name etc on top + collection: 'users' + userField: '_id' + category: 'contributor' + +### UNLOCKABLES ### +# Generally ordered according to user.stats schema +unlockableAchievements = + signup: + name: 'Signed Up' + description: 'Signed up to the most awesome coding game around.' + query: 'anonymous': false + worth: 10 + collection: 'users' + userField: '_id' + category: 'miscellaneous' + difficulty: 1 + recalculable: true + + completedFirstLevel: + name: 'Completed 1 Level' + description: 'Completed your very first level.' + query: 'stats.gamesCompleted': $gte: 1 + worth: 20 + collection: 'users' + userField: '_id' + category: 'levels' + difficulty: 1 + recalculable: true + + completedFiveLevels: + name: 'Completed 5 Levels' + description: 'Completed 5 Levels.' + query: 'stats.gamesCompleted': $gte: 5 + worth: 50 + collection: 'users' + userField: '_id' + category: 'levels' + difficulty: 2 + recalculable: true + + completedTwentyLevels: + name: 'Completed 20 Levels' + description: 'Completed 20 Levels.' + query: 'stats.gamesCompleted': $gte: 20 + worth: 500 + collection: 'users' + userField: '_id' + category: 'levels' + difficulty: 3 + recalculable: true + + editedOneArticle: contributor + name: 'Edited an Article' + description: 'Edited your first Article.' + query: 'stats.articleEdits': $gte: 1 + worth: 50 + difficulty: 1 + + editedOneLevel: contributor + name: 'Edited a Level' + description: 'Edited your first Level.' + query: 'stats.levelEdits': $gte: 1 + worth: 50 + difficulty: 1 + recalculable: true + + editedOneLevelSystem: contributor + name: 'Edited a Level System' + description: 'Edited your first Level System.' + query: 'stats.levelSystemEdits': $gte: 1 + worth: 50 + difficulty: 1 + recalculable: true + + editedOneLevelComponent: contributor + name: 'Edited a Level Component' + description: 'Edited your first Level Component.' + query: 'stats.levelComponentEdits': $gte: 1 + worth: 50 + difficulty: 1 + recalculable: true + + editedOneThangType: contributor + name: 'Edited a Thang Type' + description: 'Edited your first Thang Type.' + query: 'stats.thangTypeEdits': $gte: 1 + worth: 50 + difficulty: 1 + recalculable: true + + submittedOnePatch: contributor + name: 'Submitted a Patch' + description: 'Submitted your very first patch.' + query: 'stats.patchesSubmitted': $gte: 1 + worth: 50 + difficulty: 1 + recalculable: true + + contributedOnePatch: contributor + name: 'Contributed a Patch' + description: 'Got your very first accepted Patch.' + query: 'stats.patchesContributed': $gte: 1 + worth: 50 + difficulty: 1 + recalculable: true + + acceptedOnePatch: contributor + name: 'Accepted a Patch' + description: 'Accepted your very first patch.' + query: 'stats.patchesAccepted': $gte: 1 + worth: 50 + difficulty: 1 + recalculable: false + + oneTranslationPatch: contributor + name: 'First Translation' + description: 'Did your very first translation.' + query: 'stats.totalTranslationPatches': $gte: 1 + worth: 50 + difficulty: 1 + recalculable: true + + oneMiscPatch: contributor + name: 'First Miscellaneous Patch' + description: 'Did your first miscellaneous patch.' + query: 'stats.totalMiscPatches': $gte: 1 + worth: 50 + difficulty: 1 + recalculable: true + + oneArticleTranslationPatch: contributor + name: 'First Article Translation' + description: 'Did your very first Article translation.' + query: 'stats.articleTranslationPatches': $gte: 1 + worth: 50 + difficulty: 1 + recalculable: true + + oneArticleMiscPatch: contributor + name: 'First Misc Article Patch' + description: 'Did your first miscellaneous Article patch.' + query: 'stats.totalMiscPatches': $gte: 1 + worth: 50 + difficulty: 1 + recalculable: true + + oneLevelTranslationPatch: contributor + name: 'First Level Translation' + description: 'Did your very first Level translation.' + query: 'stats.levelTranslationPatches': $gte: 1 + worth: 50 + difficulty: 1 + recalculable: true + + oneLevelMiscPatch: contributor + name: 'First Misc Level Patch' + description: 'Did your first misc Level patch.' + query: 'stats.levelMiscPatches': $gte: 1 + worth: 50 + difficulty: 1 + recalculable: true + + +### REPEATABLES ### +repeatableAchievements = + simulatedBy: + name: 'Simulated ladder game' + description: 'Simulated a ladder game.' + query: 'simulatedBy': $gte: 1 + worth: 1 + collection: 'users' + userField: '_id' + category: 'miscellaneous' + difficulty: 1 + proportionalTo: 'simulatedBy' + function: + kind: 'logarithmic' + parameters: # TODO tweak + a: 5 + b: 1 + c: 0 + +Achievement = require '../server/achievements/Achievement' +EarnedAchievement = require '../server/achievements/EarnedAchievement' + +Achievement.find {}, (err, achievements) -> + achievementIDs = (achievement.get('_id') + '' for achievement in achievements) + EarnedAchievement.remove {achievement: $in: achievementIDs}, (err, count) -> + return log.error err if err? + log.info "Removed #{count} earned achievements that were related" + + Achievement.remove {}, (err) -> + log.error err if err? + log.info 'Removed all achievements.' + + log.info "Got #{Object.keys(unlockableAchievements).length} unlockable achievements" + log.info "and #{Object.keys(repeatableAchievements).length} repeatable achievements" + achievements = _.extend unlockableAchievements, repeatableAchievements + + async.each Object.keys(achievements), (key, callback) -> + achievement = achievements[key] + log.info "Setting up '#{achievement.name}'..." + achievementM = new Achievement achievement + # What the actual * Mongoose? It automatically converts 'stats.edits' to a nested object + achievementM.set 'query', achievement.query + log.debug JSON.stringify achievementM.get 'query' + achievementM.save (err) -> + log.error err if err? + callback() + , (err) -> + log.error err if err? + log.info 'Finished setting up achievements.' + process.exit() diff --git a/server/achievements/Achievement.coffee b/server/achievements/Achievement.coffee index fed2cfa5b..4fae21adf 100644 --- a/server/achievements/Achievement.coffee +++ b/server/achievements/Achievement.coffee @@ -1,8 +1,9 @@ mongoose = require 'mongoose' jsonschema = require '../../app/schemas/models/achievement' log = require 'winston' -util = require '../../app/lib/utils' -plugins = require '../plugins/plugins' +utils = require '../../app/lib/utils' +plugins = require('../plugins/plugins') +AchievablePlugin = require '../plugins/achievements' # `pre` and `post` are not called for update operations executed directly on the database, # including `Model.update`,`.findByIdAndUpdate`,`.findOneAndUpdate`, `.findOneAndRemove`,and `.findByIdAndRemove`.order @@ -23,23 +24,48 @@ AchievementSchema.methods.objectifyQuery = -> AchievementSchema.methods.stringifyQuery = -> @set('query', JSON.stringify(@get('query'))) if typeof @get('query') != 'string' - getExpFunction: -> - kind = @get('function')?.kind or jsonschema.function.default.kind - parameters = @get('function')?.parameters or jsonschema.function.default.parameters - return utils.functionCreators[kind](parameters) if kind of utils.functionCreators +AchievementSchema.methods.getExpFunction = -> + kind = @get('function')?.kind or jsonschema.properties.function.default.kind + parameters = @get('function')?.parameters or jsonschema.properties.function.default.parameters + return utils.functionCreators[kind](parameters) if kind of utils.functionCreators -AchievementSchema.post('init', (doc) -> doc.objectifyQuery()) +AchievementSchema.methods.isRecalculable = -> @get('recalculable') is true -AchievementSchema.pre('save', (next) -> +AchievementSchema.statics.jsonschema = jsonschema +AchievementSchema.statics.earnedAchievements = {} + +# Reloads all achievements into memory. +# TODO might want to tweak this to only load new achievements +AchievementSchema.statics.loadAchievements = (done) -> + AchievementSchema.statics.resetAchievements() + Achievement = require('../achievements/Achievement') + query = Achievement.find({}) + query.exec (err, docs) -> + _.each docs, (achievement) -> + category = achievement.get 'collection' + AchievementSchema.statics.earnedAchievements[category] = [] unless category of AchievementSchema.statics.earnedAchievements + AchievementSchema.statics.earnedAchievements[category].push achievement + done?(AchievementSchema.statics.earnedAchievements) + +AchievementSchema.statics.getLoadedAchievements = -> + AchievementSchema.statics.earnedAchievements + +AchievementSchema.statics.resetAchievements = -> + delete AchievementSchema.statics.earnedAchievements[category] for category of AchievementSchema.statics.earnedAchievements + +# Queries are stored as JSON strings, objectify them upon loading +AchievementSchema.post 'init', (doc) -> doc.objectifyQuery() + +AchievementSchema.pre 'save', (next) -> @stringifyQuery() next() -) + +# Reload achievements upon save +AchievementSchema.post 'save', -> @constructor.loadAchievements() AchievementSchema.plugin(plugins.NamedPlugin) AchievementSchema.plugin(plugins.SearchablePlugin, {searchable: ['name']}) -module.exports = Achievement = mongoose.model('Achievement', AchievementSchema) +module.exports = Achievement = mongoose.model('Achievement', AchievementSchema, 'achievements') -# Reload achievements upon save -AchievablePlugin = require '../plugins/achievements' -AchievementSchema.post 'save', (doc) -> AchievablePlugin.loadAchievements() +AchievementSchema.statics.loadAchievements() diff --git a/server/achievements/achievement_handler.coffee b/server/achievements/achievement_handler.coffee index 989a8b955..211068bd1 100644 --- a/server/achievements/achievement_handler.coffee +++ b/server/achievements/achievement_handler.coffee @@ -5,10 +5,32 @@ class AchievementHandler extends Handler modelClass: Achievement # Used to determine which properties requests may edit - editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon', 'function'] + editableProperties: ['name', 'query', 'worth', 'collection', 'description', 'userField', 'proportionalTo', 'icon', 'function', 'related', 'difficulty', 'category', 'recalculable'] + allowedMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] jsonSchema = require '../../app/schemas/models/achievement.coffee' + hasAccess: (req) -> req.method is 'GET' or req.user?.isAdmin() + get: (req, res) -> + # /db/achievement?related=