diff --git a/app/assets/images/pages/play/play-spritesheet.png b/app/assets/images/pages/play/play-spritesheet.png index f38e523e7..7d40446b5 100644 Binary files a/app/assets/images/pages/play/play-spritesheet.png and b/app/assets/images/pages/play/play-spritesheet.png differ diff --git a/app/lib/DefaultScripts.coffee b/app/lib/DefaultScripts.coffee index 818feee91..2c2fd804d 100644 --- a/app/lib/DefaultScripts.coffee +++ b/app/lib/DefaultScripts.coffee @@ -8,7 +8,7 @@ module.exports = [ focus: bounds: [{x: 0, y: 0}, {x: 80, y: 68}] target: "Hero Placeholder" - zoom: 2 + zoom: 0.5 sound: music: file: "/music/music_level_2" diff --git a/app/locale/cs.coffee b/app/locale/cs.coffee index a56b2de94..2a70e7533 100644 --- a/app/locale/cs.coffee +++ b/app/locale/cs.coffee @@ -307,7 +307,7 @@ module.exports = nativeDescription: "čeština", englishDescription: "Czech", tr tip_premature_optimization: "Předčasná optimalizace je původce všeho zla. - Donald Knuth" tip_brute_force: "V případě pochybností, použijte brute force. - Ken Thompson" tip_extrapolation: "Jsou jenom dva druhy lidí: ti, kteří mohou extrapolovat z nekompletních dat..." -# tip_superpower: "Coding is the closest thing we have to a superpower." + tip_superpower: "Kódování by se téměř dalo srovnávat se superschopnostmi." game_menu: inventory_tab: "Inventář" @@ -329,7 +329,7 @@ module.exports = nativeDescription: "čeština", englishDescription: "Czech", tr inventory: choose_inventory: "Nasadit předměty" equipped_item: "Nasazeno" -# required_purchase_title: "Required" + required_purchase_title: "Vyžadováno" available_item: "Dostupné" restricted_title: "Omezeno" should_equip: "(dvojklik pro nasazení)" @@ -482,13 +482,13 @@ module.exports = nativeDescription: "čeština", englishDescription: "Czech", tr forum_prefix: "Pro ostatní veřejné věci, prosím zkuste " forum_page: "naše fórum" forum_suffix: "." -# faq_prefix: "There's also a" -# faq: "FAQ" + faq_prefix: "Také máme " + faq: "FAQ" subscribe_prefix: "Pokud potřebujete pomoc s nějakou úrovní, prosím" subscribe: "zakupte si CodeCombat předplatné" subscribe_suffix: "a rádi vám pomůžeme s vaším kódem." subscriber_support: "Již jste CodeCombat předplatitel, takže vaše emaily budou vyřízeny dříve." -# screenshot_included: "Screenshot included." + screenshot_included: "Snímek obrazovky zahrnut." where_reply: "Kam máme odpovědět?" send: "Odeslat připomínku" contact_candidate: "Kontaktovat kandidáta" # Deprecated diff --git a/app/locale/en.coffee b/app/locale/en.coffee index ccf0c6584..26d8f38cd 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -3,8 +3,7 @@ slogan: "Learn to Code by Playing a Game" no_ie: "CodeCombat does not run in Internet Explorer 8 or older. Sorry!" # Warning that only shows up in IE8 and older no_mobile: "CodeCombat wasn't designed for mobile devices and may not work!" # Warning that shows up on mobile devices - play: "Play" # The big play button that just starts playing a level - try_it: "Try It" # Alternate wording for Play button + play: "Play" # The big play button that opens up the campaign view. old_browser: "Uh oh, your browser is too old to run CodeCombat. Sorry!" # Warning that shows up on really old Firefox/Chrome/Safari old_browser_suffix: "You can try anyway, but it probably won't work." ipad_browser: "Bad news: CodeCombat doesn't run on iPad in the browser. Good news: our native iPad app is awaiting Apple approval." @@ -658,6 +657,7 @@ achievement_query_goals: "Key achievement off of level goals" level_completion: "Level Completion" pop_i18n: "Populate I18N" + tasks: "Tasks" article: edit_btn_preview: "Preview" diff --git a/app/locale/es-419.coffee b/app/locale/es-419.coffee index 0831fea50..b3269db99 100644 --- a/app/locale/es-419.coffee +++ b/app/locale/es-419.coffee @@ -4,7 +4,6 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip no_ie: "¡Lo sentimos! CodeCombat no funciona en Internet Explorer 8 o versiones anteriores." # Warning that only shows up in IE8 and older no_mobile: "¡CodeCombat no fue diseñado para dispositivos móviles y quizás no funcione!" # Warning that shows up on mobile devices play: "Jugar" # The big play button that just starts playing a level - try_it: "Pruébalo" # Alternate wording for Play button old_browser: "¡Oh! ¡Oh! Tu navegador es muy antiguo para correr CodeCombat. ¡Lo sentimos!" # Warning that shows up on really old Firefox/Chrome/Safari old_browser_suffix: "Puedes probar de todas formas, pero probablemente no funcione." ipad_browser: "Malas noticias: CodeCombat no funciona en el navegador de iPad. Buenas noticias: nuestra propia aplicación de iPad esta en espera para ser aprobada por Apple." @@ -593,63 +592,63 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip editor: main_title: "Editor de CodeCombat" article_title: "Editor de Artículo" -# thang_title: "Thang Editor" + thang_title: "Editor de Thangs" level_title: "Editor de Nivel" -# achievement_title: "Achievement Editor" + achievement_title: "Editor de logros" back: "Atrás" revert: "Revertir" revert_models: "Revertir Modelos" pick_a_terrain: "Elije un Terreno" small: "Pequeño" grassy: "Herboso" -# fork_title: "Fork New Version" -# fork_creating: "Creating Fork..." + fork_title: "Fork de Nueva Versión" + fork_creating: "Creando Fork..." generate_terrain: "Generar terreno" more: "Más" wiki: "Wiki" live_chat: "Chat en vivo" -# thang_main: "Main" -# thang_spritesheets: "Spritesheets" -# thang_colors: "Colors" -# level_some_options: "Some Options?" -# level_tab_thangs: "Thangs" -# level_tab_scripts: "Scripts" + thang_main: "Principal" + thang_spritesheets: "Spritesheets" + thang_colors: "Colores" + level_some_options: "¿Algunas opciones?" + level_tab_thangs: "Thangs" + level_tab_scripts: "Scripts" level_tab_settings: "Opciones" level_tab_components: "Componentes" level_tab_systems: "Sistemas" level_tab_docs: "Documentación" -# level_tab_thangs_title: "Current Thangs" + level_tab_thangs_title: "Thangs Actuales" level_tab_thangs_all: "Todo" -# level_tab_thangs_conditions: "Starting Conditions" -# level_tab_thangs_add: "Add Thangs" + level_tab_thangs_conditions: "Condiciones Iniciales" + level_tab_thangs_add: "Agregar Thangs" delete: "Borrar" duplicate: "Duplicar" rotate: "Rotar" level_settings_title: "Opciones" level_component_tab_title: "Componentes Actuales" level_component_btn_new: "Crear Nuevo Componente" - level_systems_tab_title: "Sistemas Actuales Systems" - level_systems_btn_new: "Crear Nuevo Sistema New System" + level_systems_tab_title: "Sistemas Actuales" + level_systems_btn_new: "Crear Nuevo Sistema" level_systems_btn_add: "Agregar Sistema" -# level_components_title: "Back to All Thangs" + level_components_title: "Regresar a todos los Thangs" level_components_type: "Tipo" level_component_edit_title: "Editar Componente" -# level_component_config_schema: "Config Schema" + level_component_config_schema: "Config Schema" level_component_settings: "Opciones" level_system_edit_title: "Editar Sistema" create_system_title: "Crear Nuevo Sistema" new_component_title: "Crear Nuevo Componente" new_component_field_system: "Sistema" new_article_title: "Crear un Nuevo Artículo" -# new_thang_title: "Create a New Thang Type" + new_thang_title: "Crear un Nuevo tipo de Thang" new_level_title: "Crear un Nuevo Nivel" new_article_title_login: "Ingresa para Crear un Nuevo Artículo" -# new_thang_title_login: "Log In to Create a New Thang Type" + new_thang_title_login: "Ingresa para crear un nuevo tipo de Thang" new_level_title_login: "Ingresa para Crear un Nuevo Nivel" new_achievement_title: "Crear un Nuevo Logro" new_achievement_title_login: "Ingresa para Crear un Nuevo Logro" article_search_title: "Buscar Artículos aquí" -# thang_search_title: "Search Thang Types Here" + thang_search_title: "Buscar tipos de Thang aquí" level_search_title: "Buscar Niveles aquí" achievement_search_title: "Buscar logros" read_only_warning2: "Nota: no puedes guardar ediciones aquí, porque no estas logeado." @@ -663,24 +662,24 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip edit_btn_preview: "Vista previa" edit_article_title: "Editar Artículo" -# contribute: -# page_title: "Contributing" -# intro_blurb: "CodeCombat is 100% open source! Hundreds of dedicated players have helped us build the game into what it is today. Join us and write the next chapter in CodeCombat's quest to teach the world to code!" -# alert_account_message_intro: "Hey there!" -# alert_account_message: "To subscribe for class emails, you'll need to be logged in first." -# archmage_introduction: "One of the best parts about building games is they synthesize so many different things. Graphics, sound, real-time networking, social networking, and of course many of the more common aspects of programming, from low-level database management, and server administration to user facing design and interface building. There's a lot to do, and if you're an experienced programmer with a hankering to really dive into the nitty-gritty of CodeCombat, this class might be for you. We would love to have your help building the best programming game ever." -# class_attributes: "Class Attributes" -# archmage_attribute_1_pref: "Knowledge in " -# archmage_attribute_1_suf: ", or a desire to learn. Most of our code is in this language. If you're a fan of Ruby or Python, you'll feel right at home. It's JavaScript, but with a nicer syntax." -# archmage_attribute_2: "Some experience in programming and personal initiative. We'll help you get oriented, but we can't spend much time training you." -# how_to_join: "How To Join" -# join_desc_1: "Anyone can help out! Just check out our " -# join_desc_2: "to get started, and check the box below to mark yourself as a brave Archmage and get the latest news by email. Want to chat about what to do or how to get more deeply involved? " -# join_desc_3: ", or find us in our " -# join_desc_4: "and we'll go from there!" -# join_url_email: "Email us" -# join_url_hipchat: "public HipChat room" -# archmage_subscribe_desc: "Get emails on new coding opportunities and announcements." + contribute: + page_title: "Contribuyendo" + intro_blurb: "CodeCombat es 100% open source! Cientos de jugadores dedicados nos han ayudado a contruir el juego. Únete y escribe el siguiente capítulo de la misión de CodeCombat de enseñar al mundo a programar!" + alert_account_message_intro: "¡Hola!" + alert_account_message: "Para suscribirte para los correos, necesitas ingresar primero." + archmage_introduction: "Una de las mejores partes de hacer juegos es que sintetizan muchas cosas diferentes. Gráficas, sonido, redes, redes sociales y muchos aspectos comunes de programación, desde manejo de bases de datos y administración de servidores, hasta trabajar en el diseño y construcción de interfaces. Hay mucho para hacer, y si eres un programador con experiencia con el deseo de ingresar en el meollo del asunto de CodeCombat, esta clase puede ser para ti. Nos encantaría contar con tu ayuda para construir el mejor juego de programación." + class_attributes: "Atributos de Clase" + archmage_attribute_1_pref: "Conocimiento en " + archmage_attribute_1_suf: ", o un deseo de aprender. La mayor parte de nuestro código está en este lenguaje. Si eres un fan de Python o Ruby, te sentirás en casa. Es Javascript, pero con un mejor syntax." + archmage_attribute_2: "Alguna experiencia programando e iniciativa personal. Te ayudaremos a orientarte, pero no podemos perder mucho tiempo entrenando." + how_to_join: "Unirse:" + join_desc_1: "¡Cualquiera puede unirse! Sólo checa nuestro " + join_desc_2: "para comenzar, y pon un check abajo para marcarte como un valiente Archimago y conseguir las últimas noticias por email. ¿Quieres chatear sobre qué hacer o cómo involucrarte más? " + join_desc_3: ", o encuéntranos en " + join_desc_4: "y ahí empezaremos!" + join_url_email: "Escríbenos" + join_url_hipchat: "chat público HipChat" + archmage_subscribe_desc: "Obten correos de nuevas oportunidades y anuncios." # artisan_introduction_pref: "We must construct additional levels! People be clamoring for more content, and we can only build so many ourselves. Right now your workstation is level one; our level editor is barely usable even by its creators, so be wary. If you have visions of campaigns spanning for-loops to" # artisan_introduction_suf: ", then this class might be for you." # artisan_attribute_1: "Any experience in building content like this would be nice, such as using Blizzard's level editors. But not required!" diff --git a/app/locale/mk-MK.coffee b/app/locale/mk-MK.coffee index fc0a0a12c..294d8dc35 100644 --- a/app/locale/mk-MK.coffee +++ b/app/locale/mk-MK.coffee @@ -1,6 +1,6 @@ module.exports = nativeDescription: "Македонски", englishDescription: "Macedonian", translation: home: - slogan: "Научи да Програмираш Преку Игра" + slogan: "Научи да програмираш преку игра" no_ie: "CodeCombat не работи во Internet Explorer верзија 8 или постара. Извини!" # Warning that only shows up in IE8 and older no_mobile: "CodeCombat не е дизајнирана за мобилни уреди и може да не работи!" # Warning that shows up on mobile devices play: "Играј" # The big play button that just starts playing a level @@ -9,8 +9,8 @@ module.exports = nativeDescription: "Македонски", englishDescription: old_browser_suffix: "Можеш да пробаш и покрај тоа, но најверојатно нема да работи." ipad_browser: "Лоши вести: CodeCombat не работи во прелистувачот на iPad. Добри вести: Нашата апликација за iPad е готова и чека одобрение од Apple." campaign: "Кампања" - for_beginners: "За Почетници" - multiplayer: "Повеќе Играчи" # Not currently shown on home page + for_beginners: "За почетници" + multiplayer: "Повеќе играчи" # Not currently shown on home page for_developers: "За Developer-и" # Not currently shown on home page. or_ipad: "Или симни за iPad" @@ -24,7 +24,7 @@ module.exports = nativeDescription: "Македонски", englishDescription: profile: "Профил" stats: "Статистики" # code: "Code" -# admin: "Admin" # Only shows up when you are an admin + admin: "Админ" # Only shows up when you are an admin home: "Дома" contribute: "Допринеси" legal: "Законски" @@ -43,87 +43,87 @@ module.exports = nativeDescription: "Македонски", englishDescription: diplomat_suggestion: title: "Помогни да се преведе CodeCombat!" # This shows up when a player switches to a non-English language using the language selector. sub_heading: "Ни требаат твоите јазични вештини." - pitch_body: "Ние ја развиваме CodeCombat на Англиски, но веќе имаме играчи низ целиот свет. Многу од нив сакаат да играат на Македонски, а не разбираат Англиски, па ако ги зборуваш и двата јазика, размисли дали би сакал/а да се зачлениш како Дипломат и да помогнеш да се преведат на Македонски CodeCombat веб сајтот и сите нивоа од играта." - missing_translations: "Додека не преведеме сè на Македонски, содржината ќе биде на Англиски каде што Македонскиот не е достапен." + pitch_body: "Ние ја развиваме CodeCombat на англиски, но веќе имаме играчи низ целиот свет. Многу од нив сакаат да играат на македонски, а не разбираат англиски, па ако ги зборуваш и двата јазика, размисли дали би сакал/а да се зачлениш како Дипломат и да помогнеш да се преведат на македонски CodeCombat веб сајтот и сите нивоа од играта." + missing_translations: "Додека не преведеме сè на македонски, содржината ќе биде на англиски каде што македонскиот не е достапен." learn_more: "Научи повеќе за тоа како е да се биде Дипломат" subscribe_as_diplomat: "Зачлени се како Дипломат" -# play: -# play_as: "Play As" # Ladder page -# spectate: "Spectate" # Ladder page -# players: "players" # Hover over a level on /play -# hours_played: "hours played" # Hover over a level on /play -# items: "Items" # Tooltip on item shop button from /play -# unlock: "Unlock" # For purchasing items and heroes -# confirm: "Confirm" -# owned: "Owned" # For items you own -# locked: "Locked" -# purchasable: "Purchasable" # For a hero you unlocked but haven't purchased -# available: "Available" -# skills_granted: "Skills Granted" # Property documentation details -# heroes: "Heroes" # Tooltip on hero shop button from /play -# achievements: "Achievements" # Tooltip on achievement list button from /play -# account: "Account" # Tooltip on account button from /play -# settings: "Settings" # Tooltip on settings button from /play -# next: "Next" # Go from choose hero to choose inventory before playing a level -# change_hero: "Change Hero" # Go back from choose inventory to choose hero -# choose_inventory: "Equip Items" -# buy_gems: "Buy Gems" -# campaign_desert: "Desert Campaign" -# campaign_forest: "Forest Campaign" -# campaign_dungeon: "Dungeon Campaign" -# subscription_required: "Subscription Required" -# free: "Free" -# subscribed: "Subscribed" -# older_campaigns: "Older Campaigns" -# anonymous: "Anonymous Player" -# level_difficulty: "Difficulty: " -# campaign_beginner: "Beginner Campaign" -# awaiting_levels_adventurer_prefix: "We release five levels per week." -# awaiting_levels_adventurer: "Sign up as an Adventurer" -# awaiting_levels_adventurer_suffix: "to be the first to play new levels." -# choose_your_level: "Choose Your Level" # The rest of this section is the old play view at /play-old and isn't very important. -# adventurer_prefix: "You can jump to any level below, or discuss the levels on " -# adventurer_forum: "the Adventurer forum" -# adventurer_suffix: "." -# campaign_old_beginner: "Old Beginner Campaign" -# campaign_old_beginner_description: "... in which you learn the wizardry of programming." -# campaign_dev: "Random Harder Levels" -# campaign_dev_description: "... in which you learn the interface while doing something a little harder." -# campaign_multiplayer: "Multiplayer Arenas" -# campaign_multiplayer_description: "... in which you code head-to-head against other players." -# campaign_player_created: "Player-Created" -# campaign_player_created_description: "... in which you battle against the creativity of your fellow Artisan Wizards." -# campaign_classic_algorithms: "Classic Algorithms" -# campaign_classic_algorithms_description: "... in which you learn the most popular algorithms in Computer Science." + play: + play_as: "Играј како" # Ladder page + spectate: "Набљудувај" # Ladder page + players: "играчи" # Hover over a level on /play + hours_played: "часови изиграни" # Hover over a level on /play + items: "Предмети" # Tooltip on item shop button from /play + unlock: "Отклучи" # For purchasing items and heroes + confirm: "Потврди" + owned: "Во сопственост" # For items you own + locked: "Заклучено" + purchasable: "Може да се купи" # For a hero you unlocked but haven't purchased + available: "Достапно" + skills_granted: "Доделени вештини" # Property documentation details + heroes: "Херои" # Tooltip on hero shop button from /play + achievements: "Постигнувања" # Tooltip on achievement list button from /play + account: "Сметка" # Tooltip on account button from /play + settings: "Поставки" # Tooltip on settings button from /play + next: "Следно" # Go from choose hero to choose inventory before playing a level + change_hero: "Смени херој" # Go back from choose inventory to choose hero + choose_inventory: "Опреми се" + buy_gems: "Купи скапоцени камења" + campaign_desert: "Пустинска кампања" + campaign_forest: "Шумска кампања" + campaign_dungeon: "Занданска кампања" + subscription_required: "Потребно е зачленување" + free: "Бесплатно" + subscribed: "Зачленет" + older_campaigns: "Постари кампањи" + anonymous: "Анонимен играч" + level_difficulty: "Тешкотија: " + campaign_beginner: "Почетничка кампања" + awaiting_levels_adventurer_prefix: "Пуштаме пет нивоа неделно." + awaiting_levels_adventurer: "Зачлени се како Авантурист" + awaiting_levels_adventurer_suffix: "за да бидеш првиот кој ќе ги игра новите нивоа." + choose_your_level: "Избери го твоето ниво" # The rest of this section is the old play view at /play-old and isn't very important. + adventurer_prefix: "Можеш да отидеш на било кое од подолните нивоа, или да дискутираш за нивоата на " + adventurer_forum: "форумот на Авантуристите" + adventurer_suffix: "." + campaign_old_beginner: "Стара почетничка кампања" + campaign_old_beginner_description: "... во која учиш за волшепството на програмирањето." + campaign_dev: "Призволни потешки нивоа" + campaign_dev_description: "... во кои го учиш интерфејсот додека правиш нешто малку потешко." + campaign_multiplayer: "Арени за повеќе играчи" + campaign_multiplayer_description: "... во кои кодираш лице-во-лице против други играчи." + campaign_player_created: "Направено од играчи" + campaign_player_created_description: "... се бориш наспроти креативноста на останатите играчи од Волшебничкиот занает." + campaign_classic_algorithms: "Класични алгоритми" + campaign_classic_algorithms_description: "... во кои ги учиш најпопуларните алгоритми во компјутерската наука." login: - sign_up: "Направи Сметка" + sign_up: "Направи сметка" log_in: "Најави се" logging_in: "Најавувањето е во тек" log_out: "Одјави се" forgot_password: "Ја заборави својата лозинка?" authenticate_gplus: "Провери G+ најава" - load_profile: "Вчитај G+ Профил" - load_email: "Вчитај G+ Email" + load_profile: "Вчитај G+ профил" + load_email: "Вчитај G+ e-mail" finishing: "Завршување" sign_in_with_facebook: "Најави се со Facebook" sign_in_with_gplus: "Најави се со G+" signup_switch: "Сакаш да направиш сметка?" signup: - email_announcements: "Примај соопштенија преку email" + email_announcements: "Примај соопштенија преку e-mail" creating: "Сметката се прави..." - sign_up: "Направи Сметка" + sign_up: "Направи сметка" log_in: "најави се со лозинка" social_signup: "Или, можеш да се пријавиш преку Facebook или G+:" required: "Мораш да се најавиш за да имаш пристап таму." login_switch: "Веќе имаш сметка?" recover: - recover_account_title: "Врати Сметка" + recover_account_title: "Врати сметка" send_password: "Испрати лозинка за враќање" - recovery_sent: "Email-от за враќање на лозинката е испратен." + recovery_sent: "E-mail-от за враќање на лозинката е испратен." items: primary: "Главно" @@ -151,37 +151,37 @@ module.exports = nativeDescription: "Македонски", englishDescription: help: "Помош" # watch: "Watch" # unwatch: "Unwatch" - submit_patch: "Поднеси Закрпа" - submit_changes: "Поднеси Промени" + submit_patch: "Поднеси закрпа" + submit_changes: "Поднеси промени" -# general: -# and: "and" -# name: "Name" -# date: "Date" -# body: "Body" -# version: "Version" -# submitter: "Submitter" -# submitted: "Submitted" + general: + and: "и" + name: "Име" + date: "Датум" + body: "Тело" # Original was 'Body'. Not sure if this is the best translation. + version: "Верзија" + submitter: "Подносител" + submitted: "Поднесено" # commit_msg: "Commit Message" # review: "Review" -# version_history: "Version History" -# version_history_for: "Version History for: " + version_history: "Историја на верзии" + version_history_for: "Историја на верзии за: " # select_changes: "Select two changes below to see the difference." # undo: "Undo (Ctrl+Z)" # redo: "Redo (Ctrl+Shift+Z)" # play_preview: "Play preview of current level" -# result: "Result" -# results: "Results" -# description: "Description" -# or: "or" -# subject: "Subject" -# email: "Email" -# password: "Password" -# message: "Message" + result: "Резултат" + results: "Резултати" + description: "Опис" + or: "или" + subject: "Предмет на пораката" # Original: 'Subject'. Translated as 'Message subject' because in macedonian the word 'Предмет' can have different meaning. + email: "E-mail" + password: "Лозинка" + message: "Порака" # code: "Code" # ladder: "Ladder" -# when: "When" -# opponent: "Opponent" + when: "Кога" + opponent: "Противник" # rank: "Rank" # score: "Score" # win: "Win" @@ -190,7 +190,7 @@ module.exports = nativeDescription: "Македонски", englishDescription: # easy: "Easy" # medium: "Medium" # hard: "Hard" -# player: "Player" + player: "Играч" # player_level: "Level" # Like player level 5, not like level: Dungeons of Kithgard units: @@ -209,45 +209,45 @@ module.exports = nativeDescription: "Македонски", englishDescription: year: "година" years: "години" -# play_level: -# done: "Done" -# home: "Home" # Not used any more, will be removed soon. -# level: "Level" # Like "Level: Dungeons of Kithgard" -# skip: "Skip" -# game_menu: "Game Menu" -# guide: "Guide" + play_level: + done: "Готово" + home: "Дома" # Not used any more, will be removed soon. + level: "Ниво" # Like "Level: Dungeons of Kithgard" + skip: "Прескокни" + game_menu: "Мени" + guide: "Водич" # restart: "Restart" -# goals: "Goals" -# goal: "Goal" + goals: "Цели" + goal: "Цел" # running: "Running..." -# success: "Success!" -# incomplete: "Incomplete" -# timed_out: "Ran out of time" + success: "Успешно!" + incomplete: "Некомплетирано" + timed_out: "Истече времето" # failing: "Failing" # action_timeline: "Action Timeline" # click_to_select: "Click on a unit to select it." -# control_bar_multiplayer: "Multiplayer" -# control_bar_join_game: "Join Game" + control_bar_multiplayer: "Повеќе играчи" + control_bar_join_game: "Приклучи се во игра" # reload: "Reload" # reload_title: "Reload All Code?" # reload_really: "Are you sure you want to reload this level back to the beginning?" # reload_confirm: "Reload All" -# victory: "Victory" + victory: "Победа" # victory_title_prefix: "" # victory_title_suffix: " Complete" -# victory_sign_up: "Sign Up to Save Progress" -# victory_sign_up_poke: "Want to save your code? Create a free account!" -# victory_rate_the_level: "Rate the level: " # Only in old-style levels. + victory_sign_up: "Направи сметка за да го зачуваш напредокот" + victory_sign_up_poke: "Сакаш да го зачуваш твојот код? Направи бесплатна сметка!" + victory_rate_the_level: "Оцени го нивото: " # Only in old-style levels. # victory_return_to_ladder: "Return to Ladder" -# victory_play_continue: "Continue" -# victory_saving_progress: "Saving Progress" -# victory_go_home: "Go Home" # Only in old-style levels. -# victory_review: "Tell us more!" # Only in old-style levels. -# victory_hour_of_code_done: "Are You Done?" + victory_play_continue: "Продолжи" + victory_saving_progress: "Напредокот се зачувува" + victory_go_home: "Оди дома" # Only in old-style levels. + victory_review: "Кажи ни повеќе!" # Only in old-style levels. + victory_hour_of_code_done: "Дали си готов?" # victory_hour_of_code_done_yes: "Yes, I'm finished with my Hour of Code™!" -# victory_experience_gained: "XP Gained" -# victory_gems_gained: "Gems Gained" -# guide_title: "Guide" + victory_experience_gained: "Добиено искуство" + victory_gems_gained: "Добиени скапоцени камења" + guide_title: "Водич" # tome_minion_spells: "Your Minions' Spells" # Only in old-style levels. # tome_read_only_spells: "Read-Only Spells" # Only in old-style levels. # tome_other_units: "Other Units" # Only in old-style levels. @@ -326,16 +326,16 @@ module.exports = nativeDescription: "Македонски", englishDescription: # multiplayer_caption: "Play with friends!" # auth_caption: "Save your progress." -# inventory: + inventory: # choose_inventory: "Equip Items" # equipped_item: "Equipped" -# required_purchase_title: "Required" -# available_item: "Available" -# restricted_title: "Restricted" + required_purchase_title: "Задолжително" + available_item: "Достапно" + restricted_title: "Забрането" # should_equip: "(double-click to equip)" # equipped: "(equipped)" -# locked: "(locked)" -# restricted: "(restricted in this level)" + locked: "(заклучено)" + restricted: "(забрането во ова ниво)" # equip: "Equip" # unequip: "Unequip" diff --git a/app/models/LevelComponent.coffee b/app/models/LevelComponent.coffee index a54f130a7..8009827bb 100644 --- a/app/models/LevelComponent.coffee +++ b/app/models/LevelComponent.coffee @@ -14,6 +14,8 @@ module.exports = class LevelComponent extends CocoModel @PlansID: '524b7b517fc0f6d51900000d' @ProgrammableID: '524b7b5a7fc0f6d51900000e' @MovesID: '524b7b8c7fc0f6d519000013' + @MissileID: '524cc2593ea855e0ab000142' + @FindsPaths: '52872b0ead92b98561000002' urlRoot: '/db/level.component' set: (key, val, options) -> diff --git a/app/schemas/models/level.coffee b/app/schemas/models/level.coffee index 634566e29..3a4c7988b 100644 --- a/app/schemas/models/level.coffee +++ b/app/schemas/models/level.coffee @@ -16,7 +16,7 @@ defaultTasks = [ 'Choose music file in Introduction script.' 'Add to a campaign.' - 'Publish for playtesting.' + 'Publish.' 'Choose level options like required/restricted gear.' 'Create achievements, including unlocking next level.' @@ -25,17 +25,17 @@ defaultTasks = [ 'Playtest with a couple random seeds.' 'Make sure the level ends promptly on success and failure.' 'Remove/simplify unnecessary doodad collision.' - 'Release to adventurers.' + 'Release to adventurers via MailChimp.' 'Write the description.' 'Translate the sample code comments.' 'Add Io/Clojure/Lua/CoffeeScript.' 'Write the guide.' 'Write a loading tip, if needed.' - 'Populate i18n.' + 'Click the Populate i18n button.' 'Mark whether it requires a subscription (after adventurer week).' - 'Release to everyone.' + 'Release to everyone via MailChimp.' 'Check completion/engagement/problem analytics.' 'Do any custom scripting, if needed.' diff --git a/app/schemas/models/thang_type.coffee b/app/schemas/models/thang_type.coffee index e9434475e..5d4d9624e 100644 --- a/app/schemas/models/thang_type.coffee +++ b/app/schemas/models/thang_type.coffee @@ -168,6 +168,8 @@ _.extend ThangTypeSchema.properties, i18n: {type: 'object', format: 'i18n', props: ['name', 'description', 'extendedName', 'unlockLevelName'], description: 'Help translate this ThangType\'s name and description.'} extendedName: {type: 'string', title: 'Extended Hero Name', description: 'The long form of the hero\'s name. Ex.: "Captain Anya Weston".'} unlockLevelName: {type: 'string', title: 'Unlock Level Name', description: 'The name of the level in which the hero is unlocked.'} + tasks: c.array {title: 'Tasks', description: 'Tasks to be completed for this ThangType.'}, c.task + ThangTypeSchema.required = [] diff --git a/app/styles/editor/campaign/campaign-editor-view.sass b/app/styles/editor/campaign/campaign-editor-view.sass index 8ce962a8d..d55eda872 100644 --- a/app/styles/editor/campaign/campaign-editor-view.sass +++ b/app/styles/editor/campaign/campaign-editor-view.sass @@ -21,3 +21,12 @@ bottom: 0 right: 0 width: 75% + + #analytics-button + position: absolute + right: 1% + top: 1% + padding: 3px 8px + + #analytics-modal .modal-content + background-color: white diff --git a/app/templates/core/diplomat-suggestion.jade b/app/templates/core/diplomat-suggestion.jade index 0864c0493..2faddfa11 100644 --- a/app/templates/core/diplomat-suggestion.jade +++ b/app/templates/core/diplomat-suggestion.jade @@ -11,7 +11,7 @@ block modal-body-content p(data-i18n="diplomat_suggestion.missing_translations") Until we can translate everything into {English}, you'll see English when {English} isn't available. p - a(href="/contribute#diplomat", data-i18n="diplomat_suggestion.learn_more") Learn more about being a Diplomat + a(href="/contribute/diplomat", data-i18n="diplomat_suggestion.learn_more") Learn more about being a Diplomat block modal-footer-content button.btn.btn-primary.btn-large#subscribe-button(data-i18n="diplomat_suggestion.subscribe_as_diplomat") Subscribe as a Diplomat diff --git a/app/templates/editor/campaign/campaign-editor-view.jade b/app/templates/editor/campaign/campaign-editor-view.jade index 2515ed484..aa2d23855 100644 --- a/app/templates/editor/campaign/campaign-editor-view.jade +++ b/app/templates/editor/campaign/campaign-editor-view.jade @@ -40,5 +40,40 @@ block outer_content #right-column #campaign-view #campaign-level-view.hidden + if campaignDropOffs + button.btn.btn-default#analytics-button(title="Analytics", data-toggle="modal" data-target="#analytics-modal") Analytics + .modal.fade#analytics-modal(tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true") + .modal-dialog + .modal-content + .modal-header + button.close(type="button", data-dismiss="modal", aria-label="Close") + span(aria-hidden="true") × + h4.modal-title Analytics + .modal-body + if campaignDropOffs.startDay + if campaignDropOffs.endDay + div #{campaignDropOffs.startDay} to #{campaignDropOffs.endDay} + else + div #{campaignDropOffs.startDay} to today + table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt') + thead + tr + td Level + td Started + td Dropped + td Drop % + td Finished + td Dropped + td Drop % + tbody + - for (var i = 0; i < campaignDropOffs.levels.length; i++) + tr + td= campaignDropOffs.levels[i].level + td= campaignDropOffs.levels[i].started + td= campaignDropOffs.levels[i].startDropped + td= campaignDropOffs.levels[i].startDropRate + td= campaignDropOffs.levels[i].finished + td= campaignDropOffs.levels[i].finishDropped + td= campaignDropOffs.levels[i].finishDropRate block footer diff --git a/app/templates/editor/campaign/campaign-level-view.jade b/app/templates/editor/campaign/campaign-level-view.jade index e9163581c..ecdcd84b5 100644 --- a/app/templates/editor/campaign/campaign-level-view.jade +++ b/app/templates/editor/campaign/campaign-level-view.jade @@ -5,8 +5,42 @@ a(href="/editor/level/#{level.get('slug')}", target="_blank") (edit) p= level.get('description') - h2 TODO: actually put useful stuff in here + h4 Completion Rates + if levelCompletions + table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt') + thead + tr + td Date + td Started + td Finished + td Completion % + tbody + - for (var i = 0; i < levelCompletions.length; i++) + tr + td= levelCompletions[i].created + td= levelCompletions[i].started + td= levelCompletions[i].finished + td= levelCompletions[i].rate + else + div Loading... + h4 Average Playtimes + if levelPlaytimes + table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt') + thead + tr + td Date + td Average (s) + tbody + - for (var i = 0; i < levelPlaytimes.length; i++) + tr + td= levelPlaytimes[i].created + td= levelPlaytimes[i].average.toFixed(2) + else + div Loading... + + + if level.get('tasks') .tasks h3 Tasks (read only) diff --git a/app/templates/editor/thang/table.jade b/app/templates/editor/thang/table.jade index 3d4552cb8..dc2445869 100755 --- a/app/templates/editor/thang/table.jade +++ b/app/templates/editor/thang/table.jade @@ -14,6 +14,7 @@ block tableHeader th(data-i18n="general.name") Name th(data-i18n="general.description") Description th(data-i18n="general.version") Version + th(data-i18n="editor.tasks") Tasks block tableBody for thang in documents @@ -25,4 +26,10 @@ block tableBody | #{thang.get('name')} td.body-row #{thang.get('description')} - var version = thang.get('version') - td #{version.major}.#{version.minor} \ No newline at end of file + td #{version.major}.#{version.minor} + - var tasks = thang.get('tasks'); + if tasks && tasks.length + - var completed = tasks.filter(function(t) { return t.complete; }); + td #{completed.length}/#{tasks.length} + else + td diff --git a/app/templates/play/ladder_home.jade b/app/templates/play/ladder_home.jade index 77eff846f..4a12d7683 100644 --- a/app/templates/play/ladder_home.jade +++ b/app/templates/play/ladder_home.jade @@ -24,4 +24,4 @@ block content span.spl.spr - #{playCount.sessions} span(data-i18n="play.players") players .play-text-container - .overlay-text.play-text= playText + .overlay-text.play-text(data-i18n="home.play") Play diff --git a/app/views/HomeView.coffee b/app/views/HomeView.coffee index b07fbcaf2..21a7ea1c9 100644 --- a/app/views/HomeView.coffee +++ b/app/views/HomeView.coffee @@ -32,9 +32,6 @@ module.exports = class HomeView extends RootView c.explainsHourOfCode = @explainsHourOfCode c.isMobile = @isMobile() c.isIPadBrowser = @isIPadBrowser() - c.playText = $.i18n.t('home.try_it', false) - if c.playText is 'home.try_it' - c.playText = $.i18n.t 'home.play' # Temporary fallback for not having many try_it translations yet. c onClickBeginnerCampaign: (e) -> diff --git a/app/views/common/SearchView.coffee b/app/views/common/SearchView.coffee index e4ff9d179..e0b24a533 100644 --- a/app/views/common/SearchView.coffee +++ b/app/views/common/SearchView.coffee @@ -6,7 +6,7 @@ app = require 'core/application' class SearchCollection extends Backbone.Collection initialize: (modelURL, @model, @term, @projection) -> @url = "#{modelURL}?project=" - if @projection? and not (@projection == []) + if @projection?.length @url += 'created,permissions' @url += ',' + projected for projected in projection else @url += 'true' diff --git a/app/views/editor/campaign/CampaignEditorView.coffee b/app/views/editor/campaign/CampaignEditorView.coffee index 2253d0cb2..4e857851b 100644 --- a/app/views/editor/campaign/CampaignEditorView.coffee +++ b/app/views/editor/campaign/CampaignEditorView.coffee @@ -47,6 +47,8 @@ module.exports = class CampaignEditorView extends RootView @listenToOnce @levels, 'sync', @onFundamentalLoaded @listenToOnce @achievements, 'sync', @onFundamentalLoaded + _.delay @getCampaignDropOffs, 1000 + loadThangTypeNames: -> # Load the names of the ThangTypes that this level's Treema nodes might want to display. originals = [] @@ -130,6 +132,7 @@ module.exports = class CampaignEditorView extends RootView getRenderData: -> c = super() c.campaign = @campaign + c.campaignDropOffs = @campaignDropOffs c onClickSaveButton: -> @@ -236,6 +239,35 @@ module.exports = class CampaignEditorView extends RootView achievement.set 'rewards', newRewards if achievement.hasLocalChanges() @toSave.add achievement + + getCampaignDropOffs: => + # Fetch last 7 days of campaign drop-off rates + + startDay = new Date() + startDay.setDate(startDay.getUTCDate() - 6) + startDay = startDay.getUTCFullYear() + '-' + (startDay.getUTCMonth() + 1) + '-' + startDay.getUTCDate() + + success = (data) => + return if @destroyed + # API returns all the campaign data currently + @campaignDropOffs = data[@campaignHandle] + mapFn = (item) -> + item.startDropRate = (item.startDropped / item.started * 100).toFixed(2) + item.finishDropRate = (item.finishDropped / item.finished * 100).toFixed(2) + item + @campaignDropOffs.levels = _.map @campaignDropOffs.levels, mapFn, @ + @campaignDropOffs.startDay = startDay + @render() + + # TODO: Why do we need this url dash? + request = @supermodel.addRequestResource 'campaign_drop_offs', { + url: '/db/analytics_log_event/-/campaign_drop_offs' + data: {startDay: startDay, slugs: [@campaignHandle]} + method: 'POST' + success: success + }, 0 + request.load() + class LevelsNode extends TreemaObjectNode valueClass: 'treema-levels' diff --git a/app/views/editor/campaign/CampaignLevelView.coffee b/app/views/editor/campaign/CampaignLevelView.coffee index 1accea6a4..56e3df97e 100644 --- a/app/views/editor/campaign/CampaignLevelView.coffee +++ b/app/views/editor/campaign/CampaignLevelView.coffee @@ -14,11 +14,61 @@ module.exports = class CampaignLevelView extends CocoView @fullLevel.fetch() @listenToOnce @fullLevel, 'sync', => @render?() + @levelSlug = @level.get('slug') + @getLevelCompletions() + @getLevelPlaytimes() + getRenderData: -> c = super() c.level = if @fullLevel.loaded then @fullLevel else @level + c.levelCompletions = @levelCompletions + c.levelPlaytimes = @levelPlaytimes c onClickClose: -> @$el.addClass('hidden') @trigger 'hidden' + + getLevelCompletions: -> + # Fetch last 7 days of level completion counts + success = (data) => + return if @destroyed + data.sort (a, b) -> if a.created < b.created then 1 else -1 + mapFn = (item) -> + item.rate = (item.finished / item.started * 100).toFixed(2) + item + @levelCompletions = _.map data, mapFn, @ + @render() + + startDay = new Date() + startDay.setDate(startDay.getUTCDate() - 6) + startDay = startDay.getUTCFullYear() + '-' + (startDay.getUTCMonth() + 1) + '-' + startDay.getUTCDate() + + # TODO: Why do we need this url dash? + request = @supermodel.addRequestResource 'level_completions', { + url: '/db/analytics_log_event/-/level_completions' + data: {startDay: startDay, slug: @levelSlug} + method: 'POST' + success: success + }, 0 + request.load() + + getLevelPlaytimes: -> + # Fetch last 7 days of level average playtimes + success = (data) => + return if @destroyed + @levelPlaytimes = data.sort (a, b) -> if a.created < b.created then 1 else -1 + @render() + + startDay = new Date() + startDay.setDate(startDay.getUTCDate() - 6) + startDay = startDay.getUTCFullYear() + '-' + (startDay.getUTCMonth() + 1) + '-' + startDay.getUTCDate() + + # TODO: Why do we need this url dash? + request = @supermodel.addRequestResource 'playtime_averages', { + url: '/db/level/-/playtime_averages' + data: {startDay: startDay, slugs: [@levelSlug]} + method: 'POST' + success: success + }, 0 + request.load() diff --git a/app/views/editor/component/ThangComponentsEditView.coffee b/app/views/editor/component/ThangComponentsEditView.coffee index a95326a14..49c8f0f97 100644 --- a/app/views/editor/component/ThangComponentsEditView.coffee +++ b/app/views/editor/component/ThangComponentsEditView.coffee @@ -15,8 +15,8 @@ CocoCollection = require 'collections/CocoCollection' LC = (componentName, config) -> original: LevelComponent[componentName + 'ID'], majorVersion: 0, config: config DEFAULT_COMPONENTS = - Unit: [LC('Equips')] - Hero: [LC('Equips')] + Unit: [LC('Equips'), LC('FindsPaths')] + Hero: [LC('Equips'), LC('FindsPaths')] Floor: [ LC('Exists', stateless: true) LC('Physical', width: 20, height: 17, depth: 2, shape: 'sheet', pos: {x: 10, y: 8.5, z: 1}) @@ -35,6 +35,7 @@ DEFAULT_COMPONENTS = Misc: [LC('Exists'), LC('Physical')] Mark: [] Item: [LC('Item')] + Missile: [LC('Missile')] module.exports = class ThangComponentsEditView extends CocoView id: 'thang-components-edit-view' diff --git a/app/views/editor/thang/ThangTypeEditView.coffee b/app/views/editor/thang/ThangTypeEditView.coffee index c5b62b576..eae948d15 100644 --- a/app/views/editor/thang/ThangTypeEditView.coffee +++ b/app/views/editor/thang/ThangTypeEditView.coffee @@ -23,6 +23,106 @@ storage = require 'core/storage' CENTER = {x: 200, y: 300} +commonTasks = [ + 'Upload the art.' + 'Set up the vector icon.' +] + +displayedThangTypeTasks = [ + 'Configure the idle action.' + 'Configure the positions (registration point, etc.).' + 'Set shadow diameter to 0 if needed.' + 'Set scale to 0.3, 0.5, or whatever is appropriate.' + 'Set rotation to isometric if needed.' + 'Set accurate Physical size, shape, and default z.' + 'Set accurate Collides collision information if needed.' + 'Double-check that fixedRotation is accurate, if it collides.' +] + +animatedThangTypeTasks = displayedThangTypeTasks.concat [ + 'Configure the non-idle actions.' + 'Configure any per-action registration points needed.' + 'Add flipX per action if needed to face to the right.' + 'Make sure any death and attack actions do not loop.' + 'Add defaultSimlish if needed.' + 'Add selection sounds if needed.' + 'Add per-action sound triggers.' + 'Add team color groups.' +] + +containerTasks = displayedThangTypeTasks.concat [ + 'Select viable terrains if not universal.' + 'Set Exists stateless: true if needed.' +] + +purchasableTasks = [ + 'Add a tier, or 10 + desired tier if not ready yet.' + 'Add a gem cost.' + 'Write a description.' + 'Click the Populate i18n button.' +] + +defaultTasks = + Unit: commonTasks.concat animatedThangTypeTasks.concat [ + 'Start a new name category in names.coffee if needed.' + 'Set to Allied to correct team (ogres, humans, or neutral).' + 'Add AutoTargetsNearest or FightsBack if needed.' + 'Add other Components like Shoots or Casts if needed.' + 'Configure other Components, like Moves, Attackable, Attacks, etc.' + 'Override the HasAPI type if it will not be correctly inferred.' + ] + Hero: commonTasks.concat animatedThangTypeTasks.concat purchasableTasks.concat [ + 'Set the hero class.' + 'Add Extended Hero Name.' + 'Upload Hero Doll Images.' + 'Start a new name category in names.coffee.' + 'Set up hero stats in Equips, Attackable, Moves.' + 'Set Collects collectRange to 2, Sees visualRange to 60.' + 'Add any custom hero abilities.' + 'Add to ThangType model hard-coded hero ids/classes list.' + 'Add to LevelHUDView hard-coded hero short names list.' + 'Add to InventoryView hard-coded hero gender list.' + 'Add to PlayHeroesModal hard-coded hero positioning logic.' + 'Add as unlock to a level and add unlockLevelName here.' + ] + Floor: commonTasks.concat containerTasks.concat [ + 'Add 10 x 8.5 snapping.' + 'Set fixed rotation.' + 'Make sure everything is scaled to tile perfectly.' + 'Adjust SingularSprite floor scale list if necessary.' + ] + Wall: commonTasks.concat containerTasks.concat [ + 'Add 4x4 snapping.' + 'Set fixed rotation.' + 'Set up and tune complicated wall-face actions.' + 'Make sure everything is scaled to tile perfectly.' + ] + Doodad: commonTasks.concat containerTasks.concat [ + 'Add to GenerateTerrainModal logic if needed.' + ] + Misc: commonTasks.concat [ + 'Add any misc tasks for this misc ThangType.' + ] + Mark: commonTasks.concat [ + 'Check the animation framerate.' + 'Double-check that bottom of mark is just touching registration point.' + ] + Item: commonTasks.concat purchasableTasks.concat [ + 'Set the hero class if class-specific.' + 'Upload Paper Doll Images.' + ] + Missile: commonTasks.concat animatedThangTypeTasks.concat [ + 'Make sure there is a launch sound trigger.' + 'Make sure there is a hit sound trigger.' + 'Make sure there is a die animation.' + 'Add Arrow, Shell, Beam, or other missile Component.' + 'Choose Missile.leadsShots and Missile.shootsAtGround.' + 'Choose Moves.maxSpeed and other config.' + 'Choose Expires.lifespan config if needed.' + 'Set spriteType: singular if needed for proper rendering.' + 'Add HasAPI if the missile should show up in findEnemyMissiles.' + ] + module.exports = class ThangTypeEditView extends RootView id: 'thang-type-edit-view' className: 'editor' @@ -435,6 +535,8 @@ module.exports = class ThangTypeEditView extends RootView Backbone.Mediator.publish 'editor:thang-type-kind-changed', kind: kind if kind in ['Doodad', 'Floor', 'Wall'] and not @treema.data.terrains @treema.set '/terrains', ['Grass', 'Dungeon', 'Indoor', 'Desert'] # So editors know to set them. + if not @treema.data.tasks + @treema.set '/tasks', (name: t for t in defaultTasks[kind]) onSelectNode: (e, selected) => selected = selected[0] diff --git a/app/views/editor/thang/ThangTypeSearchView.coffee b/app/views/editor/thang/ThangTypeSearchView.coffee index c51351ab7..0e23c2432 100644 --- a/app/views/editor/thang/ThangTypeSearchView.coffee +++ b/app/views/editor/thang/ThangTypeSearchView.coffee @@ -6,7 +6,7 @@ module.exports = class ThangTypeSearchView extends SearchView model: require 'models/ThangType' modelURL: '/db/thang.type' tableTemplate: require 'templates/editor/thang/table' - projection: ['original', 'name', 'version', 'description', 'slug', 'kind', 'rasterIcon'] + projection: ['original', 'name', 'version', 'description', 'slug', 'kind', 'rasterIcon', 'tasks'] page: 'thang' getRenderData: -> diff --git a/bin/coco-mongodb b/bin/coco-mongodb index 18cd4cda3..a33dc1aa3 100755 --- a/bin/coco-mongodb +++ b/bin/coco-mongodb @@ -71,7 +71,7 @@ def which(cmd, mode=os.F_OK | os.X_OK, path=None): current_directory = os.path.dirname(os.path.realpath(sys.argv[0])) -allowedMongoVersions = ["v2.6.0","v2.6.1","v2.6.4","v2.6.5"] +allowedMongoVersions = ["v2.6"] if which("mongod") and any(i in subprocess.check_output("mongod --version",shell=True) for i in allowedMongoVersions): mongo_executable = "mongod" else: diff --git a/scripts/analytics/mongodb/queries/campaignRates.js b/scripts/analytics/mongodb/queries/campaignRates.js new file mode 100644 index 000000000..b41b7fb3c --- /dev/null +++ b/scripts/analytics/mongodb/queries/campaignRates.js @@ -0,0 +1,244 @@ +// Print out campaign drop-off rates +// Drop off: last started or finished level event +// Adjust startDate below for different timeframe than last 7 days. + +// Usage: +// mongo
:/ @@ -105,4 +105,4 @@ 2) Now just open 'localhost:3000' in your prefered browser. That's it, you're now ready to start working on CodeCombat! - \ No newline at end of file + diff --git a/server/analytics/analytics_log_event_handler.coffee b/server/analytics/analytics_log_event_handler.coffee index 18c5d5020..c705af981 100644 --- a/server/analytics/analytics_log_event_handler.coffee +++ b/server/analytics/analytics_log_event_handler.coffee @@ -1,5 +1,6 @@ AnalyticsLogEvent = require './AnalyticsLogEvent' Handler = require '../commons/Handler' +log = require 'winston' class AnalyticsLogEventHandler extends Handler modelClass: AnalyticsLogEvent @@ -17,4 +18,280 @@ class AnalyticsLogEventHandler extends Handler instance.set('user', req.user._id) instance + getByRelationship: (req, res, args...) -> + return @getLevelCompletionsBySlugs(req, res) if args[1] is 'level_completions' + return @getCampaignDropOffs(req, res) if args[1] is 'campaign_drop_offs' + super(arguments...) + + getLevelCompletionsBySlugs: (req, res) -> + # Returns an array of per-day level starts and finishes + # Parameters: + # slug - level slug + # startDay - Inclusive, optional, e.g. '2014-12-14' + # endDay - Exclusive, optional, e.g. '2014-12-16' + + # TODO: An uncached call takes about 15s locally + + levelSlug = req.query.slug or req.body.slug + startDay = req.query.startDay or req.body.startDay + endDay = req.query.endDay or req.body.endDay + + return @sendSuccess res, [] unless levelSlug? + + # Cache results for 1 day + @levelCompletionsCache ?= {} + @levelCompletionsCachedSince ?= new Date() + if (new Date()) - @levelCompletionsCachedSince > 86400 * 1000 # Dumb cache expiration + @levelCompletionsCache = {} + @levelCompletionsCachedSince = new Date() + cacheKey = levelSlug + cacheKey += 's' + startDay if startDay? + cacheKey += 'e' + endDay if endDay? + return @sendSuccess res, levelCompletions if levelCompletions = @levelCompletionsCache[cacheKey] + + # Build query + match = {$match: {$and: [{$or: [{"event" : 'Started Level'}, {"event" : 'Saw Victory'}]}]}} + match["$match"]["$and"].push created: {$gte: new Date(startDay + "T00:00:00.000Z")} if startDay? + match["$match"]["$and"].push created: {$lt: new Date(endDay + "T00:00:00.000Z")} if endDay? + project = {"$project": {"_id": 0, "event": 1, "level": {$ifNull: ["$properties.level", "$properties.levelID"]}, "created": {"$concat": [{"$substr": ["$created", 0, 4]}, "-", {"$substr": ["$created", 5, 2]}, "-", {"$substr" : ["$created", 8, 2]}]}}} + group = {"$group": {"_id": {"event": "$event", "created": "$created", "level": "$level"}, "count": {"$sum": 1}}} + query = AnalyticsLogEvent.aggregate match, project, group + + query.exec (err, data) => + if err? then return @sendDatabaseError res, err + + # Build per-level-day started and finished counts + levelDateMap = {} + for item in data + created = item._id.created + event = item._id.event + level = item._id.level + continue unless level? + # 'Started Level' event uses level slug, 'Saw Victory' event uses level name with caps and spaces. + level = level.toLowerCase().replace new RegExp(' ', 'g'), '-' if event is 'Saw Victory' + + levelDateMap[level] ?= {} + levelDateMap[level][created] ?= {} + levelDateMap[level][created] ?= {} + if event is 'Saw Victory' + levelDateMap[level][created]['finished'] = item.count + else + levelDateMap[level][created]['started'] = item.count + + # Build list of level completions + # Cache every level, since we had to grab all this data anyway + completions = {} + for level of levelDateMap + completions[level] = [] + for created, item of levelDateMap[level] + completions[level].push + level: level + created: created + started: item.started + finished: item.finished + cacheKey = level + cacheKey += 's' + startDay if startDay? + cacheKey += 'e' + endDay if endDay? + @levelCompletionsCache[cacheKey] = completions[level] + @sendSuccess res, completions[levelSlug] + + getCampaignDropOffs: (req, res) -> + # Returns a dictionary of per-campaign level start and finish drop-offs + # Drop-off: last started or finished level event + # Parameters: + # slugs - array of campaign slugs + # startDay - Inclusive, optional, e.g. '2014-12-14' + # endDay - Exclusive, optional, e.g. '2014-12-16' + + # TODO: Read per-campaign level progression data from a legit source + # TODO: An uncached call can take over 30s locally + # TODO: Returns all the campaigns + # TODO: Calculate overall campaign stats + + campaignSlugs = req.query.slugs or req.body.slugs + startDay = req.query.startDay or req.body.startDay + endDay = req.query.endDay or req.body.endDay + + return @sendSuccess res, [] unless campaignSlugs? + + # Cache results for 1 day + @campaignDropOffsCache ?= {} + @campaignDropOffsCachedSince ?= new Date() + if (new Date()) - @campaignDropOffsCachedSince > 86400 * 1000 # Dumb cache expiration + @campaignDropOffsCache = {} + @campaignDropOffsCachedSince = new Date() + cacheKey = campaignSlugs.join(',') + cacheKey += 's' + startDay if startDay? + cacheKey += 'e' + endDay if endDay? + return @sendSuccess res, campaignDropOffs if campaignDropOffs = @campaignDropOffsCache[cacheKey] + + queryParams = {$and: [{$or: [ {"event" : 'Started Level'}, {"event" : 'Saw Victory'}]}]} + queryParams["$and"].push created: {$gte: new Date(startDay + "T00:00:00.000Z")} if startDay? + queryParams["$and"].push created: {$lt: new Date(endDay + "T00:00:00.000Z")} if endDay? + + AnalyticsLogEvent.find(queryParams).select('created event properties user').exec (err, data) => + if err? then return @sendDatabaseError res, err + + # Bucketize events by user + userProgression = {} + for item in data + created = item.get('created') + event = item.get('event') + if event is 'Saw Victory' + level = item.get('properties.level').toLowerCase().replace new RegExp(' ', 'g'), '-' + else + level = item.get('properties.levelID') + continue unless level? + user = item.get('user') + userProgression[user] ?= [] + userProgression[user].push + created: created + event: event + level: level + + # Order user progression by created + for user in userProgression + userProgression[user].sort (a,b) -> if a.created < b.created then return -1 else 1 + + # Per-level start/drop/finish/drop + levelProgression = {} + for user of userProgression + for i in [0...userProgression[user].length] + event = userProgression[user][i].event + level = userProgression[user][i].level + levelProgression[level] ?= + started: 0 + startDropped: 0 + finished: 0 + finishDropped: 0 + if event is 'Started Level' + levelProgression[level].started++ + levelProgression[level].startDropped++ if i is userProgression[user].length - 1 + else if event is 'Saw Victory' + levelProgression[level].finished++ + levelProgression[level].finishDropped++ if i is userProgression[user].length - 1 + + # Put in campaign order + campaignRates = {} + for level of levelProgression + for campaign of campaigns + if level in campaigns[campaign] + started = levelProgression[level].started + startDropped = levelProgression[level].startDropped + finished = levelProgression[level].finished + finishDropped = levelProgression[level].finishDropped + campaignRates[campaign] ?= + levels: [] + # overall: + # started: 0, + # startDropped: 0, + # finished: 0, + # finishDropped: 0 + campaignRates[campaign].levels.push + level: level + started: started + startDropped: startDropped + finished: finished + finishDropped: finishDropped + break + + # Sort level data by campaign order + for campaign of campaignRates + campaignRates[campaign].levels.sort (a, b) -> + if campaigns[campaign].indexOf(a.level) < campaigns[campaign].indexOf(b.level) then return -1 else 1 + + # Return all campaign data for simplicity + # Cache other individual campaigns too, since we have them + @campaignDropOffsCache[cacheKey] = campaignRates + for campaign of campaignRates + cacheKey = campaign + cacheKey += 's' + startDay if startDay? + cacheKey += 'e' + endDay if endDay? + @campaignDropOffsCache[cacheKey] = campaignRates + @sendSuccess res, campaignRates + +# Copied from WorldMapView +dungeonLevels = [ + 'dungeons-of-kithgard', + 'gems-in-the-deep', + 'shadow-guard', + 'kounter-kithwise', + 'crawlways-of-kithgard', + 'forgetful-gemsmith', + 'true-names', + 'favorable-odds', + 'the-raised-sword', + 'haunted-kithmaze', + 'riddling-kithmaze', + 'descending-further', + 'the-second-kithmaze', + 'dread-door', + 'known-enemy', + 'master-of-names', + 'lowly-kithmen', + 'closing-the-distance', + 'tactical-strike', + 'the-final-kithmaze', + 'the-gauntlet', + 'kithgard-gates', + 'cavern-survival' +]; + +forestLevels = [ + 'defense-of-plainswood', + 'winding-trail', + 'patrol-buster', + 'endangered-burl', + 'village-guard', + 'thornbush-farm', + 'back-to-back', + 'ogre-encampment', + 'woodland-cleaver', + 'shield-rush', + 'peasant-protection', + 'munchkin-swarm', + 'munchkin-harvest', + 'swift-dagger', + 'shrapnel', + 'arcane-ally', + 'touch-of-death', + 'bonemender', + 'coinucopia', + 'copper-meadows', + 'drop-the-flag', + 'deadly-pursuit', + 'rich-forager', + 'siege-of-stonehold', + 'multiplayer-treasure-grove', + 'dueling-grounds' +]; + +desertLevels = [ + 'the-dunes', + 'the-mighty-sand-yak', + 'oasis', + 'sarven-road', + 'sarven-gaps', + 'thunderhooves', + 'medical-attention', + 'minesweeper', + 'sarven-sentry', + 'keeping-time', + 'hoarding-gold', + 'decoy-drill', + 'yakstraction', + 'sarven-brawl', + 'desert-combat', + 'dust', + 'mirage-maker', + 'sarven-savior', + 'odd-sandstorm' +]; + +campaigns = { + 'dungeon': dungeonLevels, + 'forest': forestLevels, + 'desert': desertLevels +} + module.exports = new AnalyticsLogEventHandler() diff --git a/server/levels/level_handler.coffee b/server/levels/level_handler.coffee index bfd56125c..03879c0c7 100644 --- a/server/levels/level_handler.coffee +++ b/server/levels/level_handler.coffee @@ -72,6 +72,7 @@ LevelHandler = class LevelHandler extends Handler return @getHistogramData(req, res, args[0]) if args[1] is 'histogram_data' return @checkExistence(req, res, args[0]) if args[1] is 'exists' return @getPlayCountsBySlugs(req, res) if args[1] is 'play_counts' + return @getLevelPlaytimesBySlugs(req, res) if args[1] is 'playtime_averages' super(arguments...) fetchLevelByIDAndHandleErrors: (id, req, res, callback) -> @@ -321,7 +322,7 @@ LevelHandler = class LevelHandler extends Handler @playCountCachedSince ?= new Date() if (new Date()) - @playCountCachedSince > 86400 * 1000 # Dumb cache expiration @playCountCache = {} - @playCountCacheSince = new Date() + @playCountCachedSince = new Date() cacheKey = levelIDs.join ',' if playCounts = @playCountCache[cacheKey] return @sendSuccess res, playCounts @@ -340,4 +341,53 @@ LevelHandler = class LevelHandler extends Handler return true if method is null or method is 'get' super(req, document, method) + + getLevelPlaytimesBySlugs: (req, res) -> + # Returns an array of per-day level average playtimes + # Parameters: + # slugs - array of level slugs + # startDay - Inclusive, optional, e.g. '2014-12-14' + # endDay - Exclusive, optional, e.g. '2014-12-16' + + # TODO: An uncached call takes about 5s for dungeons-of-kithgard locally + # TODO: This is very similar to getLevelCompletionsBySlugs(), time to generalize analytics APIs? + + levelSlugs = req.query.slugs or req.body.slugs + startDay = req.query.startDay or req.body.startDay + endDay = req.query.endDay or req.body.endDay + + return @sendSuccess res, [] unless levelSlugs? + + # Cache results for 1 day + @levelPlaytimesCache ?= {} + @levelPlaytimesCachedSince ?= new Date() + if (new Date()) - @levelPlaytimesCachedSince > 86400 * 1000 # Dumb cache expiration + @levelPlaytimesCache = {} + @levelPlaytimesCachedSince = new Date() + cacheKey = levelSlugs.join(',') + cacheKey += 's' + startDay if startDay? + cacheKey += 'e' + endDay if endDay? + return @sendSuccess res, levelPlaytimes if levelPlaytimes = @levelPlaytimesCache[cacheKey] + + # Build query + match = {$match: {$and: [{"state.complete": true}, {"playtime": {$gt: 0}}, {levelID: {$in: levelSlugs}}]}} + match["$match"]["$and"].push created: {$gte: new Date(startDay + "T00:00:00.000Z")} if startDay? + match["$match"]["$and"].push created: {$lt: new Date(endDay + "T00:00:00.000Z")} if endDay? + project = {"$project": {"_id": 0, "levelID": 1, "playtime": 1, "created": {"$concat": [{"$substr": ["$created", 0, 4]}, "-", {"$substr": ["$created", 5, 2]}, "-", {"$substr" : ["$created", 8, 2]}]}}} + group = {"$group": {"_id": {"created": "$created", "level": "$levelID"}, "average": {"$avg": "$playtime"}}} + query = Session.aggregate match, project, group + + query.exec (err, data) => + if err? then return @sendDatabaseError res, err + + # Build list of level average playtimes + playtimes = [] + for item in data + playtimes.push + level: item._id.level + created: item._id.created + average: item.average + @levelPlaytimesCache[cacheKey] = playtimes + @sendSuccess res, playtimes + module.exports = new LevelHandler() diff --git a/server/levels/thangs/thang_type_handler.coffee b/server/levels/thangs/thang_type_handler.coffee index e846816ac..9d8e195ef 100644 --- a/server/levels/thangs/thang_type_handler.coffee +++ b/server/levels/thangs/thang_type_handler.coffee @@ -33,6 +33,7 @@ ThangTypeHandler = class ThangTypeHandler extends Handler 'tier' 'extendedName' 'unlockLevelName' + 'tasks' ] hasAccess: (req) -> diff --git a/test/server/unit/analytics.spec.coffee b/test/server/unit/analytics.spec.coffee index b051c352f..c25cad7c4 100644 --- a/test/server/unit/analytics.spec.coffee +++ b/test/server/unit/analytics.spec.coffee @@ -9,9 +9,12 @@ User = require '../../../server/users/User' # TODO: these tests have some rerun/cleanup issues # TODO: add tests for purchase, payment, subscribe, unsubscribe, and earned achievements +# TODO: AnalyticsUsersActive collection isn't currently used. +# TODO: Will remove these tests if we end up ripping out the disabled saveActiveUser calls. + describe 'Analytics', -> - it 'registered user', (done) -> + xit 'registered user', (done) -> clearModels [AnalyticsUsersActive], (err) -> expect(err).toBeNull() user = new User @@ -29,7 +32,7 @@ describe 'Analytics', -> expect(activeUsers[0]?.get('event')).toEqual('register') done() - it 'level completed', (done) -> + xit 'level completed', (done) -> clearModels [AnalyticsUsersActive], (err) -> expect(err).toBeNull() unittest.getNormalJoe (joe) -> @@ -53,7 +56,7 @@ describe 'Analytics', -> expect(activeUsers[0]?.get('event')).toEqual('level-completed/lotr') done() - it 'level playtime', (done) -> + xit 'level playtime', (done) -> clearModels [AnalyticsUsersActive], (err) -> expect(err).toBeNull() unittest.getNormalJoe (joe) ->