diff --git a/app/assets/images/pages/about/sean_small.png b/app/assets/images/pages/about/sean_small.png new file mode 100644 index 000000000..3e341006b Binary files /dev/null and b/app/assets/images/pages/about/sean_small.png differ diff --git a/app/assets/images/pages/play/portal-beta-campaigns.png b/app/assets/images/pages/play/portal-beta-campaigns.png new file mode 100644 index 000000000..029dc8a3b Binary files /dev/null and b/app/assets/images/pages/play/portal-beta-campaigns.png differ diff --git a/app/locale/de-DE.coffee b/app/locale/de-DE.coffee index 09bd85636..41f1b2970 100644 --- a/app/locale/de-DE.coffee +++ b/app/locale/de-DE.coffee @@ -769,7 +769,7 @@ module.exports = nativeDescription: "Deutsch (Deutschland)", englishDescription: about: main_title: "Wenn du das Programmieren erlernen willst, musst du (viel) Code schreiben." - main_description: "Bei CodeCombat ist es unser Job das du das mit einem Lächeln im Gesicht tust." + main_description: "Bei CodeCombat ist es unser Job, dass du das mit einem Lächeln im Gesicht tust." mission_link: "Mission" team_link: "Team" story_link: "Geschichte" diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 0d33095be..dffed55df 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -304,6 +304,7 @@ signup_as_individual: "Sign up as an Individual" enter_class_code: "Enter your Class Code" enter_birthdate: "Enter your birthdate:" + parent_use_birthdate: "Parents, use your own birthdate." ask_teacher_1: "Ask your teacher for your Class Code." ask_teacher_2: "Not part of a class? Create an " ask_teacher_3: "Individual Account" @@ -475,6 +476,7 @@ victory_experience_gained: "XP Gained" victory_gems_gained: "Gems Gained" victory_new_item: "New Item" + victory_new_hero: "New Hero" victory_viking_code_school: "Holy smokes, that was a hard level you just beat! If you aren't already a software developer, you should be. You just got fast-tracked for acceptance with Viking Code School, where you can take your skills to the next level and become a professional web developer in 14 weeks." victory_become_a_viking: "Become a Viking" victory_no_progress_for_teachers: "Progress is not saved for teachers. But, you can add a student account to your classroom for yourself." @@ -571,6 +573,18 @@ tip_good_idea: "The best way to have a good idea is to have a lot of ideas. - Linus Pauling" tip_programming_not_about_computers: "Computer Science is no more about computers than astronomy is about telescopes. - Edsger Dijkstra" tip_mulan: "Believe you can, then you will. - Mulan" + + play_game_dev_level: + created_by: "Created by {{name}}" + how_to_play_title: "How to play:" + how_to_play_1: "Use the mouse to control the hero!" + how_to_play_2: "Click anywhere on the map to move to that location." + how_to_play_3: "Click on the ogres to attack them." + restart: "Restart Level" + play: "Play Level" + play_more_codecombat: "Play More CodeCombat" + default_student_instructions: "Click to control your hero and win your game!" + back_to_coding: "Back to Coding" game_menu: inventory_tab: "Inventory" @@ -804,6 +818,7 @@ elliot_title: "Partnership Manager" elliot_blurb: "Mindreader" lisa_title: "Market Development Rep" + sean_title: "Territory Manager" retrostyle_title: "Illustration" retrostyle_blurb: "RetroStyle Games" jose_title: "Music" diff --git a/app/locale/he.coffee b/app/locale/he.coffee index b730f376f..47ec2beca 100644 --- a/app/locale/he.coffee +++ b/app/locale/he.coffee @@ -1,4 +1,4 @@ - module.exports = nativeDescription: "עברית", englishDescription: "Hebrew", translation: +module.exports = nativeDescription: "עברית", englishDescription: "Hebrew", translation: home: slogan: "גם לשחק וגם ללמוד לתכנת" no_ie: "המשחק לא עובד באקפלורר 8 וישן יותר. סליחה!" # Warning that only shows up in IE8 and older diff --git a/app/locale/nl-NL.coffee b/app/locale/nl-NL.coffee index 02fc1141d..50a982434 100644 --- a/app/locale/nl-NL.coffee +++ b/app/locale/nl-NL.coffee @@ -132,7 +132,7 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription help_suff: "en we nemen contact op!" modal: -# cancel: "Cancel" + cancel: "Annuleren" close: "Sluiten" okay: "Oké" @@ -256,13 +256,13 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription signup_switch: "Wil je een account maken?" signup: -# create_student_header: "Create Student Account" -# create_teacher_header: "Create Teacher Account" -# create_individual_header: "Create Individual Account" -# create_header: "Create Account" + create_student_header: "Creëer studenten account" + create_teacher_header: "Creeër leraren account" + create_individual_header: "Creeër persoonlijk account" + create_header: "Creëer Account" email_announcements: "Ontvang aankondigingen via e-mail" # {change} creating: "Account aanmaken..." -# create_account: "Create Account" + create_account: "Creëer account" sign_up: "Aanmelden" log_in: "inloggen met wachtwoord" required: "Je moet inloggen om daarheen te gaan." @@ -270,7 +270,7 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription school_name: "Schoolnaam en stad" optional: "optioneel" school_name_placeholder: "Voorbeeld middelbare school, Amsterdam" -# connect_with: "Connect with:" + connect_with: "Verbind met:" connected_gplus_header: "Je bent ingelogd met Google+!" connected_gplus_p: "Maak je inschrijving compleet zodat je kan inloggen met je Google+ account." gplus_exists: "Jouw Google+ account is al gekoppeld!" @@ -280,35 +280,35 @@ module.exports = nativeDescription: "Nederlands (Nederland)", englishDescription hey_students: "Leerlingen, voer hier de klassencode van je docent in." birthday: "Verjaardag" # parent_email_blurb: "We know you can't wait to learn programming — we're excited too! Your parents will receive an email with further instructions on how to create an account for you. Email {{email_link}} if you have any questions." -# classroom_not_found: "No classes exist with this Class Code. Check your spelling or ask your teacher for help." -# checking: "Checking..." -# account_exists: "This email is already in use:" # {change} -# sign_in: "Sign in" -# email_good: "Email looks good!" -# name_taken: "Username already taken! Try {{suggestedName}}?" -# name_available: "Username available!" -# name_is_email: "Username may not be an email" -# choose_type: "Choose your account type:" -# teacher_type_1: "Teach programming using CodeCombat!" -# teacher_type_2: "Set up your class" + classroom_not_found: "Er zijn geen klassen met deze klassencode. Controleer je spelling of vraag je leraar om hulp." + checking: "Controleren..." + account_exists: "Dit emailadres bestaat al:" # {change} + sign_in: "Log in" + email_good: "Email ziet er goed uit!" + name_taken: "Gebruikersnaam bestaat al! Probeer {{suggestedName}}?" + name_available: "Gebruikersnaam beschikbaar!" + name_is_email: "Gebruikersnaam mag geen email zijn" + choose_type: "Kies je account type:" + teacher_type_1: "Leer ze programmeren met gebruik van CodeCombat!" + teacher_type_2: "Maak je klas" # teacher_type_3: "Access Course Guides" -# teacher_type_4: "View student progress" -# signup_as_teacher: "Sign up as a Teacher" -# student_type_1: "Learn to program while playing an engaging game!" -# student_type_2: "Play with your class" -# student_type_3: "Compete in arenas" -# student_type_4: "Choose your hero!" -# student_type_5: "Have your Class Code ready!" -# signup_as_student: "Sign up as a Student" -# individuals_or_parents: "Individuals & Parents" -# individual_type: "For players learning to code outside of a class. Parents should sign up for an account here." -# signup_as_individual: "Sign up as an Individual" -# enter_class_code: "Enter your Class Code" -# enter_birthdate: "Enter your birthdate:" -# ask_teacher_1: "Ask your teacher for your Class Code." -# ask_teacher_2: "Not part of a class? Create an " -# ask_teacher_3: "Individual Account" -# ask_teacher_4: " instead." + teacher_type_4: "Bekijk voortgang studenten" + signup_as_teacher: "Registreer als een docent" + student_type_1: "Leer programmeren terwijl je een uitdagend spel speelt!" + student_type_2: "Speel met je klas" + student_type_3: "Strijd in arena's" + student_type_4: "Kies je held!" + student_type_5: "Heb je klassencode bij de hand!" + signup_as_student: "Registreer als een student" + individuals_or_parents: "Individueel & Ouders" + individual_type: "Voor spelers die leren coderen buiten school. Ouders moeten zich hier aanmelden." + signup_as_individual: "Registreer als een individu (persoonlijk)" + enter_class_code: "Vul je klassencode in" + enter_birthdate: "Vul je geboortedatum in:" + ask_teacher_1: "Vraag je leraar om een klassencode." + ask_teacher_2: "Maak je geen deel uit een klas? Maak dan een " + ask_teacher_3: "Individuele account" + ask_teacher_4: "." # about_to_join: "You're about to join:" # enter_parent_email: "Enter your parent’s email address:" # parent_email_error: "Something went wrong when trying to send the email. Check the email address and try again." diff --git a/app/locale/pt-BR.coffee b/app/locale/pt-BR.coffee index 1b978574e..db981c752 100644 --- a/app/locale/pt-BR.coffee +++ b/app/locale/pt-BR.coffee @@ -16,7 +16,7 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: new_home: slogan: "O jogo mais envolvente para aprender programação." -# classroom_edition: "Classroom Edition:" + classroom_edition: "Editar sala de aula:" learn_to_code: "Aprenda a programar:" teacher: "Professor" student: "Aluno" @@ -25,38 +25,38 @@ module.exports = nativeDescription: "Português do Brasil", englishDescription: im_a_student: "Eu sou um Aluno" learn_more: "Aprenda mais" classroom_in_a_box: "Uma sala de aula in-a-box para o ensino de ciência da computação." -# codecombat_is: "CodeCombat is a platform for students to learn computer science while playing through a real game." -# our_courses: "Our courses have been specifically playtested to excel in the classroom, even by teachers with little to no prior programming experience." + codecombat_is: "CodeCombat é uma plataforma para estudantes aprenderem ciência da computação através de um jogo real." + our_courses: "Nossos cursos foram pensados especificamente para o uso em sala de aula, mesmo para professores com pouco conhecimento prévio em programação." top_screenshots_hint: "Alunos escrevem seus códigos e veem suas mudanças em tempo real." designed_with: "Projetado com professores em mente" real_code: "Código escrito, de verdade" from_the_first_level: "a partir do primeiro nível" -# getting_students: "Getting students to typed code as quickly as possible is critical to learning programming syntax and proper structure." + getting_students: "Fazer os estudantes digitarem o código o mais rapido possível é fundamental para aprender a sintaxe de programação e estrurutura apropriada." educator_resources: "Recursos do educador" course_guides: "e guia dos cursos" -# teaching_computer_science: "Teaching computer science does not require a costly degree, because we provide tools to support educators of all backgrounds." + teaching_computer_science: "Ensinar ciência da computação não requer um diploma oneroso, pois nós disponibilizamos ferramentas para apoiar todoos os tipos de educadores." accessible_to: "Acessível a" everyone: "todos" democratizing: "Democratizar o processo de aprendizagem de codificação é o objectivo da nossa filosofia. Qualquer um deve ser capaz de aprender a programar." forgot_learning: "Eu acho que eles realmente esqueceram que estavam aprendendo de verdade." wanted_to_do: "Programar é algo que eu sempre quis fazer, e eu nunca pensei que seria capaz de aprender isso na escola." -# why_games: "Why is learning through games important?" -# games_reward: "Games reward the productive struggle." -# encourage: "Gaming is a medium that encourages interaction, discovery, and trial-and-error. A good game challenges the player to master skills over time, which is the same critical process students go through as they learn." -# excel: "Games excel at rewarding" -# struggle: "productive struggle" -# kind_of_struggle: "the kind of struggle that results in learning that’s engaging and" + why_games: "Porque é importante aprender através de jogos?" + games_reward: "Jogos recompensam o esforço produtivo." + encourage: "Jogar é um meio de encorajar a interação, descoberta, e a tentativa e erro. Um bom jogo desafia o jogador a dominar as habilidades com o tempo, que é o mesmo processo crítico que alunos passam quando eles estão aprendendo algo." + excel: "Jogos destacam-se pela recompesa do" + struggle: "esforço produtivo" + kind_of_struggle: "o tipo de esforço resultante do aprendizado é cativante," motivating: "motivador" -# not_tedious: "not tedious." + not_tedious: "e divertido." gaming_is_good: "Estudos indicam que jogos são bons para o cérebro das crianças. (é verdade!)" -# game_based: "When game-based learning systems are" - compared: "comparado" -# conventional: "against conventional assessment methods, the difference is clear: games are better at helping students retain knowledge, concentrate and" -# perform_at_higher_level: "perform at a higher level of achievement" -# feedback: "Games also provide real-time feedback that allows students to adjust their solution path and understand concepts more holistically, instead of being limited to just “correct” or “incorrect” answers." + game_based: "Quando sistemas de aprendizado baseados em jogos são" + compared: "comparados" + conventional: "com métodos de avaliação convencional a diferença é clara: jogos são melhores e ajudam alunos a reter o conhecimento, concentração e" + perform_at_higher_level: "desenvolver um nível superior de realização" + feedback: "Jogos também fornecem respostas em tempo real permitindo que os alunos criem seus caminhos para a solução e entendam conceitos mais holisticamente, ao invés de limitar as repostas como “corretas” ou “incorretas”." real_game: "Um jogo de verdade, com programação de verdade." -# great_game: "A great game is more than just badges and achievements - it’s about a player’s journey, well-designed puzzles, and the ability to tackle challenges with agency and confidence." -# agency: "CodeCombat is a game that gives players that agency and confidence with our robust typed code engine, which helps beginner and advanced students alike write proper, valid code." + great_game: "Um grande jogo é mais do que apenas emblemas e realizações - Isso é sobre a jornada de um jogador, quebra-cabeças bem desenhados, e a habilidade para enfrentar desafios com ação e confiança." + agency: "CodeCombat é um jogo que fornece aos jogadores essa ação e confiança com nosso robusto motor de digitação de código, que ajuda alunos iniciantes e avançados tanto na escrita quanto na validação do código." # request_demo_title: "Get your students started today!" # request_demo_subtitle: "Request a demo and get your students started in less than an hour." # get_started_title: "Set up your class today" diff --git a/app/schemas/models/level.coffee b/app/schemas/models/level.coffee index 75c31b35f..120751c3f 100644 --- a/app/schemas/models/level.coffee +++ b/app/schemas/models/level.coffee @@ -278,6 +278,7 @@ LevelSchema = c.object { c.extendNamedProperties LevelSchema # let's have the name be the first property _.extend LevelSchema.properties, description: {title: 'Description', description: 'A short explanation of what this level is about.', type: 'string', maxLength: 65536, format: 'markdown'} + studentPlayInstructions: {title: 'Student Play Instructions', description: 'Instructions for game dev levels when students play them.', type: 'string', maxLength: 65536, format: 'markdown'} loadingTip: { type: 'string', title: 'Loading Tip', description: 'What to show for this level while it\'s loading.' } documentation: c.object {title: 'Documentation', description: 'Documentation articles relating to this level.', 'default': {specificArticles: [], generalArticles: []}}, specificArticles: c.array {title: 'Specific Articles', description: 'Specific documentation articles that live only in this level.', uniqueItems: true }, SpecificArticleSchema @@ -312,7 +313,7 @@ _.extend LevelSchema.properties, body: {type: 'string', format: 'markdown', title: 'Body Text', description: 'Inserted into the Victory Modal once this level is complete. Tell the player they did a good job and what they accomplished!'}, i18n: {type: 'object', format: 'i18n', props: ['body'], description: 'Help translate this victory message'} } - i18n: {type: 'object', format: 'i18n', props: ['name', 'description', 'loadingTip'], description: 'Help translate this level'} + i18n: {type: 'object', format: 'i18n', props: ['name', 'description', 'loadingTip', 'studentPlayInstructions'], description: 'Help translate this level'} icon: {type: 'string', format: 'image-file', title: 'Icon'} banner: {type: 'string', format: 'image-file', title: 'Banner'} goals: c.array {title: 'Goals', description: 'An array of goals which are visible to the player and can trigger scripts.'}, GoalSchema diff --git a/app/schemas/subscriptions/play.coffee b/app/schemas/subscriptions/play.coffee index e03736613..94032b869 100644 --- a/app/schemas/subscriptions/play.coffee +++ b/app/schemas/subscriptions/play.coffee @@ -117,6 +117,7 @@ module.exports = 'level:show-victory': c.object {required: ['showModal']}, showModal: {type: 'boolean'} + manual: { type: 'boolean' } 'level:highlight-dom': c.object {required: ['selector']}, selector: {type: 'string'} diff --git a/app/styles/modal/create-account-modal/segment-check-view.sass b/app/styles/modal/create-account-modal/segment-check-view.sass index 437209c79..815f8b816 100644 --- a/app/styles/modal/create-account-modal/segment-check-view.sass +++ b/app/styles/modal/create-account-modal/segment-check-view.sass @@ -16,3 +16,7 @@ .teacher-name font-size: 14pt + + .parent_birthdate + font-size: 11pt + margin-top: 20px diff --git a/app/styles/play/campaign-view.sass b/app/styles/play/campaign-view.sass index 5b2286cdd..255964839 100644 --- a/app/styles/play/campaign-view.sass +++ b/app/styles/play/campaign-view.sass @@ -592,6 +592,79 @@ $gameControlMargin: 30px margin: 15px 0 min-width: 100px + .beta-container + $betaImagesWidth: 1902px + width: $campaignWidth + height: $campaignHeight + display: inline-block + flex-shrink: 0 + position: relative + + .beta-campaign + width: $campaignWidth + height: $campaignHeight / 2 + display: inline-block + flex-shrink: 0 + position: relative + cursor: pointer + // http://easings.net/#easeOutBack plus tweaked a bit: http://cubic-bezier.com/#.11,.67,.08,1.42 + @include transition(0.25s cubic-bezier(0.11, 0.67, 0.8, 1.42)) + + &:hover + @include scale($campaignHoverScale) + + &.silhouette + @include filter(contrast(50%) brightness(65%)) + pointer-events: none + + &.locked + @include filter(contrast(80%) brightness(80%)) + pointer-events: none + + .campaign-label + position: absolute + top: 40% + width: 100% + text-align: center + + .campaign-name, .levels-completed, .campaign-locked + margin: 0 + color: white + text-shadow: black 2px 2px 0, black -2px -2px 0, black 2px -2px 0, black -2px 2px 0, black 2px 0px 0, black 0px -2px 0, black -2px 0px 0, black 0px 2px 0 + + .campaign-locked + margin: 32px 0 + + .campaign-description + margin: 0px 40px + background: transparent url(/images/level/popover_border_background.png) no-repeat + background-size: 100% 100% + padding: 12px + color: black + font-size: 12px + + .levels-completed + font-size: 22px + + .play-button + margin: 10px 0 + min-width: 100px + color: white + + .background-container + position: absolute + left: $campaignWidth / 4 + width: $campaignWidth / 2 + height: $campaignHeight / 2 + background: transparent url(/images/pages/play/portal-beta-campaigns.png) no-repeat 0 0 + background-size: $betaImagesWidth / 2 + + &.campaign-web-dev-1 + background-position: -151px 0px + &.campaign-game-dev-1 + background-position: -454px 0px + + .small-nav-logo, .picoctf-powered-by position: absolute top: 1% diff --git a/app/styles/play/level/tome/spell_palette_entry.sass b/app/styles/play/level/tome/spell_palette_entry.sass index 73b1e5c73..5a0a0e345 100644 --- a/app/styles/play/level/tome/spell_palette_entry.sass +++ b/app/styles/play/level/tome/spell_palette_entry.sass @@ -52,6 +52,7 @@ body.dialogue-view-active .spell-palette-popover.popover // Only those popovers which are our direct children (spell documentation) + top: 80px !important left: auto !important right: 45% max-width: 600px diff --git a/app/templates/about.jade b/app/templates/about.jade index a0be8a7fe..0eaf9e3d4 100644 --- a/app/templates/about.jade +++ b/app/templates/about.jade @@ -146,6 +146,13 @@ block content small(data-i18n="about.lisa_title") br + li + img(src="/images/pages/about/sean_small.png").img-thumbnail + .team-bio + h6.label.team-name Sean McNulty + small(data-i18n="about.sean_title") + br + // Part time / contract li a(href="http://floor.is/lava/" rel="external") diff --git a/app/templates/core/create-account-modal/segment-check-view.jade b/app/templates/core/create-account-modal/segment-check-view.jade index e79b1eeee..0127a5115 100644 --- a/app/templates/core/create-account-modal/segment-check-view.jade +++ b/app/templates/core/create-account-modal/segment-check-view.jade @@ -50,6 +50,7 @@ form.modal-body.segment-check - var thisYear = new Date().getFullYear() for year in _.range(thisYear, thisYear - 100, -1) option(selected=(year == view.signupState.get('birthdayYear'))) #{year} + .parent_birthdate(data-i18n="signup.parent_use_birthdate") default p diff --git a/app/templates/editor/article/edit.jade b/app/templates/editor/article/edit.jade index 551881872..dd0005c14 100644 --- a/app/templates/editor/article/edit.jade +++ b/app/templates/editor/article/edit.jade @@ -22,12 +22,9 @@ block content #article-treema - #article-view - h3(data-i18n="resources.patches") Patches .patches-view hr div#error-view - diff --git a/app/templates/new-home-view.jade b/app/templates/new-home-view.jade index 752b16224..6f7c77ec8 100644 --- a/app/templates/new-home-view.jade +++ b/app/templates/new-home-view.jade @@ -7,7 +7,7 @@ mixin box div a.teacher-btn.btn.btn-primary.btn-lg.btn-block(data-event-action="Homepage Click Teacher Button CTA", data-i18n="new_home.im_a_teacher") div - a.student-btn.btn.btn-forest.btn-lg.btn-block(href="/courses", data-event-action="Homepage Click Student Button CTA", data-i18n="new_home.im_a_student") + a.student-btn.btn.btn-forest.btn-lg.btn-block(href="/home#create-account-student", data-event-action="Homepage Click Student Button CTA", data-i18n="new_home.im_a_student") h6#learn-to-code-header(data-i18n="new_home.learn_to_code") a.btn.btn-gold.btn-lg.btn-block.play-btn(href=view.playURL, data-event-action="Homepage Play Now CTA", data-i18n="new_home.play_now") diff --git a/app/templates/play/campaign-view.jade b/app/templates/play/campaign-view.jade index 8433d32b8..1dad493ef 100644 --- a/app/templates/play/campaign-view.jade +++ b/app/templates/play/campaign-view.jade @@ -100,28 +100,57 @@ if view.showAds() else .portal .portals - for campaignSlug in ['dungeon', 'forest', 'desert', 'mountain', 'glacier', 'volcano'] - - var campaign = campaigns[campaignSlug]; - - var godmode = me.get('permissions', true).indexOf('godmode') != -1; - div(class="campaign #{campaignSlug}" + (campaign ? "" : " silhouette") + (campaign && campaign.locked && !godmode ? " locked" : ""), data-campaign-slug=campaignSlug) - .campaign-label - h2.campaign-name - if campaign - span= i18n(campaign.attributes, 'fullName') - else - span ??? - if campaign && campaign.levelsTotal - h3.levels-completed - span= campaign.levelsCompleted - | / - span= campaign.levelsTotal - if campaign && campaign.locked && !godmode - h3.campaign-locked(data-i18n="play.locked") Locked - else if campaign - btn(data-i18n="common.play").btn.btn-illustrated.btn-lg.btn-success.play-button - if campaign && campaign.get('description') - p.campaign-description - span= i18n(campaign.attributes, 'description') + for campaignSlug in ['dungeon', 'beta-campaigns', 'forest', 'desert', 'mountain', 'glacier', 'volcano'] + if campaignSlug === 'beta-campaigns' + - var betaSlugs = _.shuffle(['campaign-game-dev-1', 'campaign-web-dev-1']); + .beta-container + each campaignSlug in betaSlugs + - var campaign = campaigns[campaignSlug]; + if !campaign + - continue; + div(class="beta-campaign" + (campaign ? "" : " silhouette") + (campaign && campaign.locked && !godmode ? " locked" : ""), data-campaign-slug=campaignSlug) + .background-container(class="#{campaignSlug}") + .campaign-label + h3.campaign-name + if campaign + span= i18n(campaign.attributes, 'fullName') + if campaign.levelsTotal + span.spl= campaign.levelsCompleted + | / + span= campaign.levelsTotal + else + span ??? + if campaign && campaign.locked && !godmode + h4.campaign-locked(data-i18n="play.locked") Locked + else if campaign + btn.btn.btn-illustrated.btn-lg.btn-primary.play-button(data-i18n="common.play") + if campaign && campaign.get('description') + p.campaign-description + span= i18n(campaign.attributes, 'description') + else + - var campaign = campaigns[campaignSlug]; + if !campaign + - continue; + - var godmode = me.get('permissions', true).indexOf('godmode') != -1; + div(class="campaign #{campaignSlug}" + (campaign ? "" : " silhouette") + (campaign && campaign.locked && !godmode ? " locked" : ""), data-campaign-slug=campaignSlug) + .campaign-label + h2.campaign-name + if campaign + span= i18n(campaign.attributes, 'fullName') + else + span ??? + if campaign && campaign.levelsTotal + h3.levels-completed + span= campaign.levelsCompleted + | / + span= campaign.levelsTotal + if campaign && campaign.locked && !godmode + h3.campaign-locked(data-i18n="play.locked") Locked + else if campaign + btn(data-i18n="common.play").btn.btn-illustrated.btn-lg.btn-success.play-button + if campaign && campaign.get('description') + p.campaign-description + span= i18n(campaign.attributes, 'description') .game-controls.header-font.picoctf-hide button.btn.poll.hidden(data-i18n="[title]play.poll") diff --git a/app/templates/play/level/modal/hero-victory-modal.jade b/app/templates/play/level/modal/hero-victory-modal.jade index c1b651c82..5bf75373f 100644 --- a/app/templates/play/level/modal/hero-victory-modal.jade +++ b/app/templates/play/level/modal/hero-victory-modal.jade @@ -79,7 +79,10 @@ block modal-body-content .reward-panel.hero(data-hero-thang-type=hero.get('original')) .reward-image-container(class=animate ? 'pending-reward-image' : 'show') img(src=hero.getPortraitURL()) - .reward-text= animate ? 'New Hero' : hero.get('name') + if animate + .reward-text(data-i18n="play_level.victory_new_hero") New Hero + else + .reward-text= i18n(hero.attributes, 'name') if rewards.items for item in rewards.items diff --git a/app/templates/play/level/play-game-dev-level-view.jade b/app/templates/play/level/play-game-dev-level-view.jade index f7d1c89eb..fcc0166aa 100644 --- a/app/templates/play/level/play-game-dev-level-view.jade +++ b/app/templates/play/level/play-game-dev-level-view.jade @@ -15,7 +15,7 @@ if view.level.id && view.session.id h3.m-y-1= view.level.get('name') - h4 Created by #{view.session.get('creatorName')} + h4= view.state.get('creatorString') hr if view.state.get('loading') @@ -24,24 +24,24 @@ .progress-bar(style="width: #{view.state.get('progress')}") if ready - h3 Goals + h3(data-i18n="play_level.goals") for goalName in view.state.get('goalNames') p= goalName hr - h3 How to play: - p Use the mouse to control the hero! - p Click anywhere on the map to move to that location. - p Click on the ogres to attack them. + h3(data-i18n="play_game_dev_level.how_to_play_title") + p(data-i18n="play_game_dev_level.how_to_play_1") + p(data-i18n="play_game_dev_level.how_to_play_2") + p(data-i18n="play_game_dev_level.how_to_play_3") if ready .panel-footer - var playing = view.state.get('playing') if playing - button#play-btn.btn.btn-lg.btn-burgandy.btn-block Restart Level + button#play-btn.btn.btn-lg.btn-burgandy.btn-block(data-i18n="play_game_dev_level.restart") else - button#play-btn.btn.btn-lg.btn-forest.btn-block Play Level + button#play-btn.btn.btn-lg.btn-forest.btn-block(data-i18n="play_game_dev_level.play") #share-row.m-t-3 if ready @@ -55,4 +55,4 @@ span(data-i18n='sharing.copy_url') .panel-body - a#play-more-codecombat-btn.btn.btn-lg.btn-navy-alt.pull-right(href="/") Play More CodeCombat + a#play-more-codecombat-btn.btn.btn-lg.btn-navy-alt.pull-right(href="/", data-i18n="play_game_dev_level.play_more_codecombat") diff --git a/app/templates/play/play-level-view.jade b/app/templates/play/play-level-view.jade index ef8f92ee5..c481bf911 100644 --- a/app/templates/play/play-level-view.jade +++ b/app/templates/play/play-level-view.jade @@ -52,17 +52,15 @@ if view.showAds() button.btn.btn-lg.btn-warning.banner.header-font#stop-real-time-playback-button(title="Stop real-time playback") if view.level && view.level.isType('game-dev') - | Back to coding + span(data-i18n="play_game_dev_level.back_to_coding") else span(data-i18n="play_level.skip") #how-to-play-game-dev-panel.panel.panel-default.hide.style-flat .panel-heading - h3.panel-title How to play: - .panel-body - p Use the mouse to control the hero! - p Click anywhere on the map to move to that location. - p Click on the ogres to attack them. + h3.panel-title(data-i18n="play_game_dev_level.how_to_play_title") + + .panel-body!= view.howToPlayText .hints-view.hide diff --git a/app/views/NewHomeView.coffee b/app/views/NewHomeView.coffee index 3b75505fc..3c222881f 100644 --- a/app/views/NewHomeView.coffee +++ b/app/views/NewHomeView.coffee @@ -84,13 +84,14 @@ module.exports = class NewHomeView extends RootView onClickStudentButton: (e) -> window.tracker?.trackEvent $(e.target).data('event-action'), category: 'Homepage', [] + @render?() if document.location.href.search('/home#create-account-student') isnt -1 onClickTeacherButton: (e) -> window.tracker?.trackEvent $(e.target).data('event-action'), category: 'Homepage', [] if me.isTeacher() application.router.navigate('/teachers', { trigger: true }) else - @scrollToLink('.request-demo-row', 600) + application.router.navigate('/teachers/signup', { trigger: true }) onClickViewProfile: (e) -> window.tracker?.trackEvent $(e.target).data('event-action'), category: 'Homepage', [] diff --git a/app/views/editor/level/settings/SettingsTabView.coffee b/app/views/editor/level/settings/SettingsTabView.coffee index 88951e2a5..a2be48d71 100644 --- a/app/views/editor/level/settings/SettingsTabView.coffee +++ b/app/views/editor/level/settings/SettingsTabView.coffee @@ -16,7 +16,7 @@ module.exports = class SettingsTabView extends CocoView 'name', 'description', 'documentation', 'nextLevel', 'background', 'victory', 'i18n', 'icon', 'goals', 'type', 'terrain', 'showsGuide', 'banner', 'employerDescription', 'loadingTip', 'requiresSubscription', 'helpVideos', 'replayable', 'scoreTypes', 'concepts', 'picoCTFProblem', 'practice', 'practiceThresholdMinutes' - 'shareable' + 'shareable', 'studentPlayInstructions' ] subscriptions: diff --git a/app/views/i18n/I18NEditLevelView.coffee b/app/views/i18n/I18NEditLevelView.coffee index 375d67c3a..5c866f349 100644 --- a/app/views/i18n/I18NEditLevelView.coffee +++ b/app/views/i18n/I18NEditLevelView.coffee @@ -17,6 +17,8 @@ module.exports = class I18NEditLevelView extends I18NEditModelView @wrapRow 'Level description', ['description'], description, i18n[lang]?.description, [] if loadingTip = @model.get('loadingTip') @wrapRow 'Loading tip', ['loadingTip'], loadingTip, i18n[lang]?.loadingTip, [] + if studentPlayInstructions = @model.get('studentPlayInstructions') + @wrapRow 'Student Play Instructions', ['studentPlayInstructions'], studentPlayInstructions, i18n[lang]?.studentPlayInstructions, [] # goals for goal, index in @model.get('goals') ? [] diff --git a/app/views/play/CampaignView.coffee b/app/views/play/CampaignView.coffee index 7e24df30d..e9e07e609 100644 --- a/app/views/play/CampaignView.coffee +++ b/app/views/play/CampaignView.coffee @@ -55,6 +55,7 @@ module.exports = class CampaignView extends RootView 'click #back-button': 'onClickBack' 'click #clear-storage-button': 'onClickClearStorage' 'click .portal .campaign': 'onClickPortalCampaign' + 'click .portal .beta-campaign': 'onClickPortalCampaign' 'mouseenter .portals': 'onMouseEnterPortals' 'mouseleave .portals': 'onMouseLeavePortals' 'mousemove .portals': 'onMouseMovePortals' @@ -365,7 +366,7 @@ module.exports = class CampaignView extends RootView return experienceScore createLine: (o1, o2) -> - mapHeight = parseFloat($(".map").css("height")) + mapHeight = parseFloat($(".map").css("height")) mapWidth = parseFloat($(".map").css("width")) return unless mapHeight > 0 ratio = mapWidth / mapHeight @@ -668,7 +669,7 @@ module.exports = class CampaignView extends RootView console.error "CampaignView hero update couldn't find hero slug for original:", hero onClickPortalCampaign: (e) -> - campaign = $(e.target).closest('.campaign') + campaign = $(e.target).closest('.campaign, .beta-campaign') return if campaign.is('.locked') or campaign.is('.silhouette') campaignSlug = campaign.data('campaign-slug') Backbone.Mediator.publish 'router:navigate', diff --git a/app/views/play/level/PlayGameDevLevelView.coffee b/app/views/play/level/PlayGameDevLevelView.coffee index f4c4b17bc..f75cb8c54 100644 --- a/app/views/play/level/PlayGameDevLevelView.coffee +++ b/app/views/play/level/PlayGameDevLevelView.coffee @@ -33,6 +33,7 @@ module.exports = class PlayGameDevLevelView extends RootView @state = new State({ loading: true progress: 0 + creatorString: '' }) @supermodel.on 'update-progress', (progress) => @@ -92,6 +93,7 @@ module.exports = class PlayGameDevLevelView extends RootView loading: false goalNames shareURL + creatorString: $.i18n.t('play_game_dev_level.created_by').replace('{{name}}', @session.get('creatorName')) }) @eventProperties = { category: 'Play GameDev Level' diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee index f8ad85119..3488d1ec5 100644 --- a/app/views/play/level/PlayLevelView.coffee +++ b/app/views/play/level/PlayLevelView.coffee @@ -211,7 +211,12 @@ module.exports = class PlayLevelView extends RootView @$el.addClass 'web-dev' # Hide some of the elements we won't be using return @world = @levelLoader.world - @$el.addClass 'game-dev' if @level.isType('game-dev') + if @level.isType('game-dev') + @$el.addClass 'game-dev' + @howToPlayText = utils.i18n(@level.attributes, 'studentPlayInstructions') + @howToPlayText ?= $.i18n.t('play_game_dev_level.default_student_instructions') + @howToPlayText = marked(@howToPlayText, { sanitize: true }) + @renderSelectors('#how-to-play-game-dev-panel') @$el.addClass 'hero' if @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev') # TODO: figure out what this does and comment it @$el.addClass 'flags' if _.any(@world.thangs, (t) -> (t.programmableProperties and 'findFlags' in t.programmableProperties) or t.inventory?.flag) or @level.get('slug') is 'sky-span' # TODO: Update terminology to always be opponentSession or otherSession @@ -548,9 +553,9 @@ module.exports = class PlayLevelView extends RootView onDonePressed: -> @showVictory() - onShowVictory: (e) -> + onShowVictory: (e={}) -> $('#level-done-button').show() unless @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev') - @showVictory() if e.showModal + @showVictory(_.pick(e, 'manual')) if e.showModal return if @victorySeen @victorySeen = true victoryTime = (new Date()) - @loadEndTime @@ -563,8 +568,9 @@ module.exports = class PlayLevelView extends RootView ls: @session?.get('_id') application.tracker?.trackTiming victoryTime, 'Level Victory Time', @levelID, @levelID - showVictory: -> + showVictory: (options={}) -> return if @level.hasLocalChanges() # Don't award achievements when beating level changed in level editor + return if @level.isType('game-dev') and @level.get('shareable') and not options.manual @endHighlight() options = {level: @level, supermodel: @supermodel, session: @session, hasReceivedMemoryWarning: @hasReceivedMemoryWarning, courseID: @courseID, courseInstanceID: @courseInstanceID, world: @world} ModalClass = if @level.isType('hero', 'hero-ladder', 'hero-coop', 'course', 'course-ladder', 'game-dev', 'web-dev') then HeroVictoryModal else VictoryModal @@ -638,18 +644,21 @@ module.exports = class PlayLevelView extends RootView if @level.isType('game-dev') panel = @$('#how-to-play-game-dev-panel') panel.removeClass('hide') - lines = switch @level.get('slug') - when 'over-the-garden-wall' then ['Watch to see if the peasants are properly protected.'] - when 'click-gait' then ['Move to each red "X".', 'Click on the screen to move the Knight there.'] - when 'heros-journey' then ['Move to each red "X".', 'Click on the screen to move the Knight there.'] - when 'a-maze-ing' then ['Move to the chest of gems.', 'Click on the screen to move the Duelist there.'] - when 'gemtacular' then ['Move to each of the gems.', 'Click on the screen to move the Captain there.'] - when 'vorpal-mouse' then ['Slay the ogres.', 'Click on the screen to move the Guardian there.', 'Click on the munchkins to attack them!'] - when 'crushing-it' then ['Slay the ogres.', 'Click on the screen to move the Goliath there.', 'Click on the munchkins to attack them!'] - when 'tabula-rasa' then ['Slay any ogres.', 'Collect any coins.', 'Click on the screen to move the Raider there.', 'Click on any munchkins to attack them!'] - else ['Click to control your hero and win your game!'] - html = _.map(lines, (line) -> "
#{line}
").join('') - panel.find('.panel-body').html(html) + # TODO: Remove this once these levels have studentPlayInstructions set. + if not @level.get('studentPlayInstructions') + lines = switch @level.get('slug') + when 'over-the-garden-wall' then ['Watch to see if the peasants are properly protected.'] + when 'click-gait' then ['Move to each red "X".', 'Click on the screen to move the Knight there.'] + when 'heros-journey' then ['Move to each red "X".', 'Click on the screen to move the Knight there.'] + when 'a-maze-ing' then ['Move to the chest of gems.', 'Click on the screen to move the Duelist there.'] + when 'gemtacular' then ['Move to each of the gems.', 'Click on the screen to move the Captain there.'] + when 'vorpal-mouse' then ['Slay the ogres.', 'Click on the screen to move the Guardian there.', 'Click on the munchkins to attack them!'] + when 'crushing-it' then ['Slay the ogres.', 'Click on the screen to move the Goliath there.', 'Click on the munchkins to attack them!'] + when 'tabula-rasa' then ['Slay any ogres.', 'Collect any coins.', 'Click on the screen to move the Raider there.', 'Click on any munchkins to attack them!'] + else null + if lines + html = _.map(lines, (line) -> "#{line}
").join('') + panel.find('.panel-body').html(html) @onWindowResize() diff --git a/app/views/play/level/modal/HeroVictoryModal.coffee b/app/views/play/level/modal/HeroVictoryModal.coffee index 3e7ce32a6..af5517513 100644 --- a/app/views/play/level/modal/HeroVictoryModal.coffee +++ b/app/views/play/level/modal/HeroVictoryModal.coffee @@ -324,7 +324,7 @@ module.exports = class HeroVictoryModal extends ModalView @playSound 'item-unlocked' if 0.5 < ratio < 0.6 else if panel.hero thangType = @thangTypes[panel.hero] - panel.textEl.text(thangType.get('name')) + panel.textEl.text utils.i18n(thangType.attributes, 'name') @playSelectionSound thangType if 0.5 < ratio < 0.6 if ratio is 1 panel.rootEl.removeClass('animating').find('.reward-image-container img').removeClass('pulse') @@ -413,7 +413,17 @@ module.exports = class HeroVictoryModal extends ModalView AudioPlayer.playSound name, 1 getNextLevelCampaign: -> - {'kithgard-gates': 'forest', 'kithgard-mastery': 'forest', 'siege-of-stonehold': 'desert', 'clash-of-clones': 'mountain', 'summits-gate': 'glacier'}[@level.get('slug')] or @level.get 'campaign' # Much easier to just keep this updated than to dynamically figure it out. + # Much easier to just keep this updated than to dynamically figure it out. + # TODO: only go back to world selector if any beta campaigns are incomplete + { + 'kithgard-gates': '', + 'kithgard-mastery': '', + 'tabula-rasa': '', + 'wanted-poster': '', + 'siege-of-stonehold': '', + 'clash-of-clones': 'mountain', + 'summits-gate': 'glacier' + }[@level.get('slug')] ? @level.get 'campaign' getNextLevelLink: (returnToCourse=false) -> if @level.isType('course', 'game-dev', 'web-dev') and nextLevel = @level.get('nextLevel') and not returnToCourse diff --git a/app/views/play/level/tome/CastButtonView.coffee b/app/views/play/level/tome/CastButtonView.coffee index 2d1427142..de652c040 100644 --- a/app/views/play/level/tome/CastButtonView.coffee +++ b/app/views/play/level/tome/CastButtonView.coffee @@ -81,7 +81,7 @@ module.exports = class CastButtonView extends CocoView onDoneButtonClick: (e) -> return if @options.level.hasLocalChanges() # Don't award achievements when beating level changed in level editor @options.session.recordScores @world?.scores, @options.level - Backbone.Mediator.publish 'level:show-victory', showModal: true + Backbone.Mediator.publish 'level:show-victory', { showModal: true, manual: true } onSpellChanged: (e) -> @updateCastButton() diff --git a/app/views/play/level/tome/editor/zatanna.coffee b/app/views/play/level/tome/editor/zatanna.coffee index 57907b17e..8a672351e 100644 --- a/app/views/play/level/tome/editor/zatanna.coffee +++ b/app/views/play/level/tome/editor/zatanna.coffee @@ -198,10 +198,10 @@ module.exports = class Zatanna editor.completer.autoInsert = false editor.completer.showPopup(editor) - # Hide popup if more than 10 suggestions + # Hide popup if too many suggestions # TODO: Completions aren't asked for unless we show popup, so this is super hacky # TODO: Backspacing to yield more suggestions does not close popup - if editor.completer?.completions?.filtered?.length > 10 + if editor.completer?.completions?.filtered?.length > 20 editor.completer.detach() # Update popup CSS after it's been launched @@ -244,6 +244,7 @@ module.exports = class Zatanna addCodeCombatSnippets: (level, spellView, e) -> snippetEntries = [] + source = spellView.getSource() haveFindNearestEnemy = false haveFindNearest = false for group, props of e.propGroups @@ -279,7 +280,6 @@ module.exports = class Zatanna else 'while true' # For now, update autocomplete to use hero instead of self/this, if hero is already used in the source. # Later, we should make this happen all the time - or better yet update the snippets. - source = spellView.getSource() if /hero/.test(source) or not /(self[\.\:]|this\.|\@)/.test(source) thisToken = 'python': /self/, @@ -318,6 +318,15 @@ module.exports = class Zatanna attackEntry.content = attackEntry.content.replace '${1:enemy}', '"${1:Enemy Name}"' snippetEntries.push attackEntry + # Add copied hero. entries for most important ones that start with hero. + sortedEntries = _.sortBy snippetEntries, (entry) -> -1 * parseInt(entry.importance ? 0) + for entry in sortedEntries + if entry.content?.indexOf('hero.') is 0 + newEntry = _.cloneDeep(entry) + entry.name = "hero.#{newEntry.name}" + snippetEntries.push(newEntry) + break if snippetEntries.length - sortedEntries.length >= 10 + if haveFindNearest and not haveFindNearestEnemy spellView.translateFindNearest() diff --git a/scripts/addZenProspectLeadsToClose.js b/scripts/addZenProspectLeadsToClose.js index ccf90fd3a..1ea79e13b 100644 --- a/scripts/addZenProspectLeadsToClose.js +++ b/scripts/addZenProspectLeadsToClose.js @@ -51,6 +51,16 @@ function createCloseLead(zpContact, done) { if (zpContact.phone) { postData.contacts[0].phones = [{phone: zpContact.phone}]; } + if (zpContact.district) { + postData.custom['demo_nces_district'] = zpContact.district; + postData.custom['demo_nces_name'] = zpContact.organization; + } + if (zpContact.nces_district_id) { + postData.custom['demo_nces_district_id'] = zpContact.nces_district_id; + } + if (zpContact.nces_school_id) { + postData.custom['demo_nces_id'] = zpContact.nces_school_id; + } const options = { uri: `https://${closeIoApiKey}:X@app.close.io/api/v1/lead/`, body: JSON.stringify(postData) @@ -146,16 +156,31 @@ function getZPContactsPage(contacts, searchQuery, done) { if (err) return done(err); const data = JSON.parse(body); for (let contact of data.contacts) { - let organization = contact.organization_name; - if (contact.custom_fields && contact.custom_fields.school_name) organization = contact.custom_fields.school_name; - contacts.push({ - organization: organization, + const newContact = { + organization: contact.organization_name, name: contact.name, title: contact.title, email: contact.email, phone: contact.phone, data: contact - }); + }; + // Check custom fields, school_name set means organization_name is district name + if (contact.custom_fields) { + if (contact.custom_fields.school_name) { + newContact.district = contact.organization_name; + newContact.organization = contact.custom_fields.school_name; + // console.log(`DEBUG: found contact with school name ${newContact.email} ${contact.custom_fields.school_name}`); + } + if (contact.custom_fields.nces_district_id) { + newContact.nces_district_id = contact.custom_fields.nces_district_id; + // console.log(`DEBUG: found contact with district id ${newContact.email} ${newContact.nces_district_id}`); + } + if (contact.custom_fields.nces_school_id) { + newContact.nces_school_id = contact.custom_fields.nces_school_id; + // console.log(`DEBUG: found contact with school id ${newContact.email} ${newContact.nces_school_id}`); + } + } + contacts.push(newContact); } return done(null, data.pipeline_total); }); diff --git a/server/handlers/level_handler.coffee b/server/handlers/level_handler.coffee index eb82beade..03435ddf0 100644 --- a/server/handlers/level_handler.coffee +++ b/server/handlers/level_handler.coffee @@ -68,7 +68,8 @@ LevelHandler = class LevelHandler extends Handler 'scoreTypes' 'concepts' 'picoCTFProblem' - 'practiceThresholdMinutes' + 'practiceThresholdMinutes', + 'studentPlayInstructions' ] postEditableProperties: ['name'] diff --git a/server/middleware/users.coffee b/server/middleware/users.coffee index c4edafe99..f18a457a6 100644 --- a/server/middleware/users.coffee +++ b/server/middleware/users.coffee @@ -107,7 +107,7 @@ module.exports = getStudents: wrap (req, res, next) -> throw new errors.Unauthorized('You must be an administrator.') unless req.user?.isAdmin() query = $or: [{role: 'student'}, {$and: [{schoolName: {$exists: true}}, {schoolName: {$ne: ''}}, {anonymous: false}]}] - users = yield User.find(query).select('lastIP schoolName').lean() + users = yield User.find(query).select('lastIP').lean() for user in users if ip = user.lastIP user.geo = geoip.lookup(ip) diff --git a/test/app/views/teachers/RequestQuoteView.spec.coffee b/test/app/views/teachers/RequestQuoteView.spec.coffee index 060d50375..d08ae9a10 100644 --- a/test/app/views/teachers/RequestQuoteView.spec.coffee +++ b/test/app/views/teachers/RequestQuoteView.spec.coffee @@ -48,7 +48,7 @@ describe 'RequestQuoteView', -> } }]) }) - _.defer done # Let SuperModel finish + view.supermodel.once('loaded-all', done) it 'shows request received', -> expect(view.$('#request-form').hasClass('hide')).toBe(true) @@ -220,7 +220,7 @@ describe 'RequestQuoteView', -> } }]) }) - _.defer done # Let SuperModel finish + view.supermodel.once('loaded-all', done) it 'shows form with data from the most recent request', -> expect(view.$('input[name="firstName"]').val()).toBe('First')