From ec99c93059f220745365f1bdeb6fea36a53e4408 Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Mon, 29 Dec 2014 23:20:43 -0500 Subject: [PATCH 01/28] Make mongo 2.6 version selection less specific --- bin/coco-mongodb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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: From 1df017b27119b6668e51418fb78e868ba7f3c593 Mon Sep 17 00:00:00 2001 From: Michael Schmatz Date: Mon, 29 Dec 2014 23:28:59 -0500 Subject: [PATCH 02/28] Changed latest to 2.6.6 to fix #2007 --- scripts/devSetup/mongo.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/devSetup/mongo.py b/scripts/devSetup/mongo.py index eb57ea452..46814ce8d 100644 --- a/scripts/devSetup/mongo.py +++ b/scripts/devSetup/mongo.py @@ -86,10 +86,10 @@ class LinuxMongoDBDownloader(MongoDBDownloader): @property def download_url(self): if self.dependency.config.mem_width == 64: - return u"http://fastdl.mongodb.org/linux/mongodb-linux-x86_64-latest.tgz" + return u"http://fastdl.mongodb.org/linux/mongodb-linux-x86_64-2.6.6.tgz" else: warnings.warn(u"MongoDB *really* doesn't run well on 32 bit systems. You have been warned.") - return u"http://fastdl.mongodb.org/linux/mongodb-linux-i686-latest.tgz" + return u"http://fastdl.mongodb.org/linux/mongodb-linux-i686-2.6.6.tgz" class WindowsMongoDBDownloader(MongoDBDownloader): @property @@ -97,13 +97,13 @@ class WindowsMongoDBDownloader(MongoDBDownloader): #TODO: Implement Windows Vista detection warnings.warn(u"If you have a version of Windows older than 7, MongoDB may not function properly!") if self.dependency.config.mem_width == 64: - return u"http://fastdl.mongodb.org/win32/mongodb-win32-x86_64-2008plus-latest.zip" + return u"http://fastdl.mongodb.org/win32/mongodb-win32-x86_64-2008plus-2.6.6.zip" else: - return u"http://fastdl.mongodb.org/win32/mongodb-win32-i386-latest.zip" + return u"http://fastdl.mongodb.org/win32/mongodb-win32-i386-2.6.6.zip" class MacMongoDBDownloader(MongoDBDownloader): @property def download_url(self): - return u"http://fastdl.mongodb.org/osx/mongodb-osx-x86_64-latest.tgz" + return u"http://fastdl.mongodb.org/osx/mongodb-osx-x86_64-2.6.6.tgz" From cac2a5b7fa28e08af65ab3e17e156cbf10fd44d9 Mon Sep 17 00:00:00 2001 From: Ivan Milanov Date: Tue, 30 Dec 2014 07:36:16 +0100 Subject: [PATCH 03/28] Added translations in the 'play' segment --- app/locale/mk-MK.coffee | 98 ++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/app/locale/mk-MK.coffee b/app/locale/mk-MK.coffee index fc0a0a12c..275a48100 100644 --- a/app/locale/mk-MK.coffee +++ b/app/locale/mk-MK.coffee @@ -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: "Законски" @@ -48,54 +48,54 @@ module.exports = nativeDescription: "Македонски", englishDescription: 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: "Направи Сметка" From de334b8ced82c94fa1241e2e27a8ba25aa348a02 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Mon, 29 Dec 2014 23:18:24 -0800 Subject: [PATCH 04/28] Campaign drop off rates script Shows where players stop playing. --- .../mongodb/queries/campaignRates.js | 244 ++++++++++++++++++ .../analytics/mongodb/queries/levelRates.js | 4 - 2 files changed, 244 insertions(+), 4 deletions(-) create mode 100644 scripts/analytics/mongodb/queries/campaignRates.js 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 + From 82aa972fbcd380ac6c4a1124267b558cda825340 Mon Sep 17 00:00:00 2001 From: "Omar S." Date: Tue, 30 Dec 2014 10:42:19 -0500 Subject: [PATCH 10/28] Create tips-es.coco --- .../coco-dev-setup/batch/config/localized/tips-es.coco | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 scripts/windows/coco-dev-setup/batch/config/localized/tips-es.coco diff --git a/scripts/windows/coco-dev-setup/batch/config/localized/tips-es.coco b/scripts/windows/coco-dev-setup/batch/config/localized/tips-es.coco new file mode 100644 index 000000000..911890eb9 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/config/localized/tips-es.coco @@ -0,0 +1,6 @@ +1) Si tienes una pregunta, hazla con cuidado y detalles + 2) Esta instalacion esta en beta y puede tener errores + 3) Puedes reportar bugs en @ 'https://github.com/codecombat/codecombat/issues' + 4) Tienes preguntas o sugerencias? Habla con nosotros en HipChat @ https://www.hipchat.com/g3plnOKqa + + Puedes encontrar una guia paso a paso para esta instalacion en @ https://github.com/codecombat/codecombat/wiki/Setup-on-Windows:-a-step-by-step-guide From 02f5f70318cfb3ef1106b7377f2f9e98d98cee4a Mon Sep 17 00:00:00 2001 From: "Omar S." Date: Tue, 30 Dec 2014 10:46:03 -0500 Subject: [PATCH 11/28] Create readme-es.coco --- .../batch/config/localized/readme-es.coco | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 scripts/windows/coco-dev-setup/batch/config/localized/readme-es.coco diff --git a/scripts/windows/coco-dev-setup/batch/config/localized/readme-es.coco b/scripts/windows/coco-dev-setup/batch/config/localized/readme-es.coco new file mode 100644 index 000000000..3978aa569 --- /dev/null +++ b/scripts/windows/coco-dev-setup/batch/config/localized/readme-es.coco @@ -0,0 +1,29 @@ + _____ _ _____ _ _ + / __ \ | | / __ \ | | | | + | / \/ ___ __| | ___ | / \/ ___ _ __ ___ | |__ __ _| |_ + | | / _ \ / _` |/ _ \ | | / _ \| '_ ` _ \| '_ \ / _` | __| + | \__/\ (_) | (_| | __/ | \__/\ (_) | | | | | | |_) | (_| | |_ + \____/\___/ \__,_|\___| \____/\___/|_| |_| |_|_.__/ \__,_|\__| + +============================================================================= + +Felicidades, ahora eres parte dela comunidad de CodeCombat. +Ahora que el ambiente de desarrollo ha sido instalado, estas listo para comenzar +a contribuir y hacer este mundo un mejor lugar. + +Tienes preguntas o te gustaria conocernos? +Habla con nosotros en hipchat @ https://www.hipchat.com/g3plnOKqa + +Tambien puedes hablar con nosotros en nuestro foro. +El foro esta en @ http://discourse.codecombat.com/ + +Puedes leer sobre los ultimos cambios en nuestro blog. +El blog esta en @ http://blog.codecombat.com/ + +Por ultimo, puedes encontrar casi toda nuestra documentacion +e informacion en nuestra wiki @ https://github.com/codecombat/codecombat/wiki + +Esperemos que disfrutes tanto la comunidad como nosotros + + + - Nick, George, Scott, Michael, Jeremy and Glen From 63763cf2b9c3b417f0910f06388a9c33b77acb5a Mon Sep 17 00:00:00 2001 From: "Omar S." Date: Tue, 30 Dec 2014 10:50:35 -0500 Subject: [PATCH 12/28] Update readme-es.coco --- .../coco-dev-setup/batch/config/localized/readme-es.coco | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/windows/coco-dev-setup/batch/config/localized/readme-es.coco b/scripts/windows/coco-dev-setup/batch/config/localized/readme-es.coco index 3978aa569..00d46c4ce 100644 --- a/scripts/windows/coco-dev-setup/batch/config/localized/readme-es.coco +++ b/scripts/windows/coco-dev-setup/batch/config/localized/readme-es.coco @@ -1,4 +1,4 @@ - _____ _ _____ _ _ + _____ _ _____ _ _ / __ \ | | / __ \ | | | | | / \/ ___ __| | ___ | / \/ ___ _ __ ___ | |__ __ _| |_ | | / _ \ / _` |/ _ \ | | / _ \| '_ ` _ \| '_ \ / _` | __| From d14546cd14b73dc41efa0cb00cdcef965fd10e26 Mon Sep 17 00:00:00 2001 From: "Omar S." Date: Tue, 30 Dec 2014 10:51:08 -0500 Subject: [PATCH 13/28] Update tips-es.coco Sorry for always doing this tiny mistakes --- .../windows/coco-dev-setup/batch/config/localized/tips-es.coco | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/windows/coco-dev-setup/batch/config/localized/tips-es.coco b/scripts/windows/coco-dev-setup/batch/config/localized/tips-es.coco index 911890eb9..16c4c6ddb 100644 --- a/scripts/windows/coco-dev-setup/batch/config/localized/tips-es.coco +++ b/scripts/windows/coco-dev-setup/batch/config/localized/tips-es.coco @@ -1,4 +1,4 @@ -1) Si tienes una pregunta, hazla con cuidado y detalles + 1) Si tienes una pregunta, hazla con cuidado y detalles 2) Esta instalacion esta en beta y puede tener errores 3) Puedes reportar bugs en @ 'https://github.com/codecombat/codecombat/issues' 4) Tienes preguntas o sugerencias? Habla con nosotros en HipChat @ https://www.hipchat.com/g3plnOKqa From 88f050ba0640d64fb906acede980f2a7244f4f83 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Tue, 30 Dec 2014 12:18:32 -0800 Subject: [PATCH 14/28] Disable invalid analytics server tests --- test/server/unit/analytics.spec.coffee | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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) -> From 72be5c35f0f35762e81714253fe22cc47cd08cd1 Mon Sep 17 00:00:00 2001 From: Imperadeiro98 Date: Tue, 30 Dec 2014 21:38:07 +0000 Subject: [PATCH 15/28] Fixed link --- app/templates/core/diplomat-suggestion.jade | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From cf72abd2a21e02ed0fce6019110cdd49c250888d Mon Sep 17 00:00:00 2001 From: Martin005 Date: Wed, 31 Dec 2014 00:21:22 +0100 Subject: [PATCH 16/28] Update cs.coffee --- app/locale/cs.coffee | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 From 9e9d69ec9b17b47bea177fa8e113063a4a4db8db Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Tue, 30 Dec 2014 16:24:31 -0800 Subject: [PATCH 17/28] Removed try_it text (which was accidentally used in the wrong place anyway). --- app/locale/en.coffee | 3 +-- app/templates/play/ladder_home.jade | 2 +- app/views/HomeView.coffee | 3 --- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/app/locale/en.coffee b/app/locale/en.coffee index ccf0c6584..945ecc668 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." 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) -> From 58499392e825c54920f996a86d38837f5544a11c Mon Sep 17 00:00:00 2001 From: "Omar S." Date: Tue, 30 Dec 2014 20:22:00 -0500 Subject: [PATCH 18/28] Update es-419.coffee Upp to line 667 --- app/locale/es-419.coffee | 47 ++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/app/locale/es-419.coffee b/app/locale/es-419.coffee index 0831fea50..1dc37e855 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,9 +662,9 @@ 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!" + 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: "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." From 9e8c34420868c84988e0b78c3ce00c0fefe447cf Mon Sep 17 00:00:00 2001 From: "Omar S." Date: Tue, 30 Dec 2014 23:29:14 -0500 Subject: [PATCH 19/28] Update es-419.coffee More more more --- app/locale/es-419.coffee | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/app/locale/es-419.coffee b/app/locale/es-419.coffee index 1dc37e855..b3269db99 100644 --- a/app/locale/es-419.coffee +++ b/app/locale/es-419.coffee @@ -665,21 +665,21 @@ module.exports = nativeDescription: "Español (América Latina)", englishDescrip 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: "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." + 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!" From fcf5346aa3e5bafc16c7033171aaba92df333ff3 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Wed, 31 Dec 2014 11:49:17 -0800 Subject: [PATCH 20/28] Add completion rates to levels in campaign editor --- .../editor/campaign/campaign-level-view.jade | 19 ++++- .../editor/campaign/CampaignLevelView.coffee | 28 +++++++ .../analytics_log_event_handler.coffee | 73 +++++++++++++++++++ 3 files changed, 119 insertions(+), 1 deletion(-) diff --git a/app/templates/editor/campaign/campaign-level-view.jade b/app/templates/editor/campaign/campaign-level-view.jade index e9163581c..75b7f6550 100644 --- a/app/templates/editor/campaign/campaign-level-view.jade +++ b/app/templates/editor/campaign/campaign-level-view.jade @@ -5,7 +5,24 @@ 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-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... if level.get('tasks') .tasks diff --git a/app/views/editor/campaign/CampaignLevelView.coffee b/app/views/editor/campaign/CampaignLevelView.coffee index 1accea6a4..f8a1f20e1 100644 --- a/app/views/editor/campaign/CampaignLevelView.coffee +++ b/app/views/editor/campaign/CampaignLevelView.coffee @@ -14,11 +14,39 @@ module.exports = class CampaignLevelView extends CocoView @fullLevel.fetch() @listenToOnce @fullLevel, 'sync', => @render?() + @levelSlug = @level.get('slug') + @getLevelCompletions() + getRenderData: -> c = super() c.level = if @fullLevel.loaded then @fullLevel else @level + c.levelCompletions = @levelCompletions 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, slugs: [@levelSlug]} + method: 'POST' + success: success + }, 0 + request.load() diff --git a/server/analytics/analytics_log_event_handler.coffee b/server/analytics/analytics_log_event_handler.coffee index 18c5d5020..ab02ba8a2 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,76 @@ 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' + super(arguments...) + + getLevelCompletionsBySlugs: (req, res) -> + # Returns an array of per-day level starts and finishes + # 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 15s + + 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 + @levelCompletionsCache ?= {} + @levelCompletionsCachedSince ?= new Date() + if (new Date()) - @levelCompletionsCachedSince > 86400 * 1000 # Dumb cache expiration + @levelCompletionsCache = {} + @levelCompletionsCacheSince = new Date() + cacheKey = levelSlugs.join(',') + 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' + continue unless level in levelSlugs + + 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 + completions = [] + for level of levelDateMap + for created, item of levelDateMap[level] + completions.push + level: level + created: created + started: item.started + finished: item.finished + @levelCompletionsCache[cacheKey] = completions + @sendSuccess res, completions + module.exports = new AnalyticsLogEventHandler() From 9b6d327c7f6c9e3a3cbb316e49ed99e5ca6fa590 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Wed, 31 Dec 2014 12:25:18 -0800 Subject: [PATCH 21/28] Add average playtimes to levels in campaign editor --- .../editor/campaign/campaign-level-view.jade | 17 ++++++ .../editor/campaign/CampaignLevelView.coffee | 22 ++++++++ .../analytics_log_event_handler.coffee | 2 +- server/levels/level_handler.coffee | 53 +++++++++++++++++++ 4 files changed, 93 insertions(+), 1 deletion(-) diff --git a/app/templates/editor/campaign/campaign-level-view.jade b/app/templates/editor/campaign/campaign-level-view.jade index 75b7f6550..b27157aaa 100644 --- a/app/templates/editor/campaign/campaign-level-view.jade +++ b/app/templates/editor/campaign/campaign-level-view.jade @@ -24,6 +24,23 @@ else div Loading... + h4 Average Playtimes + if levelPlaytimes + 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/views/editor/campaign/CampaignLevelView.coffee b/app/views/editor/campaign/CampaignLevelView.coffee index f8a1f20e1..cdbf8872f 100644 --- a/app/views/editor/campaign/CampaignLevelView.coffee +++ b/app/views/editor/campaign/CampaignLevelView.coffee @@ -16,11 +16,13 @@ module.exports = class CampaignLevelView extends CocoView @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: -> @@ -50,3 +52,23 @@ module.exports = class CampaignLevelView extends CocoView 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/server/analytics/analytics_log_event_handler.coffee b/server/analytics/analytics_log_event_handler.coffee index ab02ba8a2..afdb6068b 100644 --- a/server/analytics/analytics_log_event_handler.coffee +++ b/server/analytics/analytics_log_event_handler.coffee @@ -28,7 +28,7 @@ class AnalyticsLogEventHandler extends Handler # 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 15s levelSlugs = req.query.slugs or req.body.slugs diff --git a/server/levels/level_handler.coffee b/server/levels/level_handler.coffee index bfd56125c..f789fcd92 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) -> @@ -340,4 +341,56 @@ 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 20s 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 = {} + @levelPlaytimesCacheSince = 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}}]}} + 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 + created = item._id.created + level = item._id.level + continue unless level? and level in levelSlugs + playtimes.push + level: level + created: created + average: item.average + @levelPlaytimesCache[cacheKey] = playtimes + @sendSuccess res, playtimes + module.exports = new LevelHandler() From 56b43465566f5898e777b16ed37d785ec5c4c05a Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Wed, 31 Dec 2014 13:19:46 -0800 Subject: [PATCH 22/28] Update average playtimes query to match level --- app/templates/editor/campaign/campaign-level-view.jade | 4 ++-- server/levels/level_handler.coffee | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/app/templates/editor/campaign/campaign-level-view.jade b/app/templates/editor/campaign/campaign-level-view.jade index b27157aaa..ecdcd84b5 100644 --- a/app/templates/editor/campaign/campaign-level-view.jade +++ b/app/templates/editor/campaign/campaign-level-view.jade @@ -7,7 +7,7 @@ h4 Completion Rates if levelCompletions - table.table-bordered.table-condensed.table-hover(style='font-size:10pt') + table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt') thead tr td Date @@ -26,7 +26,7 @@ h4 Average Playtimes if levelPlaytimes - table.table-bordered.table-condensed.table-hover(style='font-size:10pt') + table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt') thead tr td Date diff --git a/server/levels/level_handler.coffee b/server/levels/level_handler.coffee index f789fcd92..23562cbc4 100644 --- a/server/levels/level_handler.coffee +++ b/server/levels/level_handler.coffee @@ -370,7 +370,7 @@ LevelHandler = class LevelHandler extends Handler return @sendSuccess res, levelPlaytimes if levelPlaytimes = @levelPlaytimesCache[cacheKey] # Build query - match = {$match: {$and: [{"state.complete": true}, {"playtime": {$gt: 0}}]}} + 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]}]}}} @@ -383,12 +383,9 @@ LevelHandler = class LevelHandler extends Handler # Build list of level average playtimes playtimes = [] for item in data - created = item._id.created - level = item._id.level - continue unless level? and level in levelSlugs playtimes.push - level: level - created: created + level: item._id.level + created: item._id.created average: item.average @levelPlaytimesCache[cacheKey] = playtimes @sendSuccess res, playtimes From b0056127af8c1530573bed5c99d34587c75b23c8 Mon Sep 17 00:00:00 2001 From: George Saines Date: Wed, 31 Dec 2014 15:23:33 -0800 Subject: [PATCH 23/28] changing the xp icon in the play view --- .../images/pages/play/play-spritesheet.png | Bin 17273 -> 18829 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/app/assets/images/pages/play/play-spritesheet.png b/app/assets/images/pages/play/play-spritesheet.png index f38e523e7de2d8455031887d61e21e691d256b6f..7d40446b55fef8f2c05b2f5ccd3c725efe8adee2 100644 GIT binary patch delta 18129 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 delta 16561 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 dzyIelU@(^Hy!~>! Date: Wed, 31 Dec 2014 16:54:58 -0800 Subject: [PATCH 24/28] Fixed #2036 (making it less likely that the mistake will ercur, anyway). --- app/lib/DefaultScripts.coffee | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" From e65887ec7924d385150ba3f2bac0e7146d2c62ff Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Thu, 1 Jan 2015 12:01:43 -0800 Subject: [PATCH 25/28] Add campaign drop-offs analytics to editor --- .../editor/campaign/campaign-editor-view.sass | 9 + .../editor/campaign/campaign-editor-view.jade | 35 +++ .../editor/campaign/CampaignEditorView.coffee | 32 +++ .../analytics_log_event_handler.coffee | 204 +++++++++++++++++- server/levels/level_handler.coffee | 6 +- 5 files changed, 281 insertions(+), 5 deletions(-) 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/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/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/server/analytics/analytics_log_event_handler.coffee b/server/analytics/analytics_log_event_handler.coffee index afdb6068b..8115a5fcf 100644 --- a/server/analytics/analytics_log_event_handler.coffee +++ b/server/analytics/analytics_log_event_handler.coffee @@ -20,6 +20,7 @@ class AnalyticsLogEventHandler extends Handler 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) -> @@ -29,7 +30,7 @@ class AnalyticsLogEventHandler extends Handler # startDay - Inclusive, optional, e.g. '2014-12-14' # endDay - Exclusive, optional, e.g. '2014-12-16' - # TODO: An uncached call takes about 15s + # TODO: An uncached call takes about 15s locally levelSlugs = req.query.slugs or req.body.slugs startDay = req.query.startDay or req.body.startDay @@ -42,7 +43,7 @@ class AnalyticsLogEventHandler extends Handler @levelCompletionsCachedSince ?= new Date() if (new Date()) - @levelCompletionsCachedSince > 86400 * 1000 # Dumb cache expiration @levelCompletionsCache = {} - @levelCompletionsCacheSince = new Date() + @levelCompletionsCachedSince = new Date() cacheKey = levelSlugs.join(',') cacheKey += 's' + startDay if startDay? cacheKey += 'e' + endDay if endDay? @@ -90,4 +91,203 @@ class AnalyticsLogEventHandler extends Handler @levelCompletionsCache[cacheKey] = completions @sendSuccess res, completions + 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 23562cbc4..03879c0c7 100644 --- a/server/levels/level_handler.coffee +++ b/server/levels/level_handler.coffee @@ -322,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 @@ -349,7 +349,7 @@ LevelHandler = class LevelHandler extends Handler # startDay - Inclusive, optional, e.g. '2014-12-14' # endDay - Exclusive, optional, e.g. '2014-12-16' - # TODO: An uncached call takes about 20s for dungeons-of-kithgard locally + # 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 @@ -363,7 +363,7 @@ LevelHandler = class LevelHandler extends Handler @levelPlaytimesCachedSince ?= new Date() if (new Date()) - @levelPlaytimesCachedSince > 86400 * 1000 # Dumb cache expiration @levelPlaytimesCache = {} - @levelPlaytimesCacheSince = new Date() + @levelPlaytimesCachedSince = new Date() cacheKey = levelSlugs.join(',') cacheKey += 's' + startDay if startDay? cacheKey += 'e' + endDay if endDay? From 2d410fa57f6cee3963142821cb26cc91f7f59aa8 Mon Sep 17 00:00:00 2001 From: Matt Lott Date: Thu, 1 Jan 2015 12:26:19 -0800 Subject: [PATCH 26/28] Update editor analytics level completions We have to grab all the level data at once, so we should cache it all too. Only the first level completions call should be uncached/slow. --- .../editor/campaign/CampaignLevelView.coffee | 2 +- .../analytics_log_event_handler.coffee | 22 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/app/views/editor/campaign/CampaignLevelView.coffee b/app/views/editor/campaign/CampaignLevelView.coffee index cdbf8872f..56e3df97e 100644 --- a/app/views/editor/campaign/CampaignLevelView.coffee +++ b/app/views/editor/campaign/CampaignLevelView.coffee @@ -47,7 +47,7 @@ module.exports = class CampaignLevelView extends CocoView # TODO: Why do we need this url dash? request = @supermodel.addRequestResource 'level_completions', { url: '/db/analytics_log_event/-/level_completions' - data: {startDay: startDay, slugs: [@levelSlug]} + data: {startDay: startDay, slug: @levelSlug} method: 'POST' success: success }, 0 diff --git a/server/analytics/analytics_log_event_handler.coffee b/server/analytics/analytics_log_event_handler.coffee index 8115a5fcf..c705af981 100644 --- a/server/analytics/analytics_log_event_handler.coffee +++ b/server/analytics/analytics_log_event_handler.coffee @@ -26,17 +26,17 @@ class AnalyticsLogEventHandler extends Handler getLevelCompletionsBySlugs: (req, res) -> # Returns an array of per-day level starts and finishes # Parameters: - # slugs - array of level slugs + # 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 - levelSlugs = req.query.slugs or req.body.slugs + 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 levelSlugs? + return @sendSuccess res, [] unless levelSlug? # Cache results for 1 day @levelCompletionsCache ?= {} @@ -44,7 +44,7 @@ class AnalyticsLogEventHandler extends Handler if (new Date()) - @levelCompletionsCachedSince > 86400 * 1000 # Dumb cache expiration @levelCompletionsCache = {} @levelCompletionsCachedSince = new Date() - cacheKey = levelSlugs.join(',') + cacheKey = levelSlug cacheKey += 's' + startDay if startDay? cacheKey += 'e' + endDay if endDay? return @sendSuccess res, levelCompletions if levelCompletions = @levelCompletionsCache[cacheKey] @@ -69,7 +69,6 @@ class AnalyticsLogEventHandler extends Handler 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' - continue unless level in levelSlugs levelDateMap[level] ?= {} levelDateMap[level][created] ?= {} @@ -80,16 +79,21 @@ class AnalyticsLogEventHandler extends Handler levelDateMap[level][created]['started'] = item.count # Build list of level completions - 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.push + completions[level].push level: level created: created started: item.started finished: item.finished - @levelCompletionsCache[cacheKey] = completions - @sendSuccess res, completions + 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 From dbc42fb7e11d94e5fe0c9b86079ec55faf3daf68 Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Thu, 1 Jan 2015 13:47:42 -0800 Subject: [PATCH 27/28] Added task system to Thang Editor. --- app/models/LevelComponent.coffee | 2 + app/schemas/models/level.coffee | 8 +- app/schemas/models/thang_type.coffee | 2 + .../component/ThangComponentsEditView.coffee | 5 +- .../editor/thang/ThangTypeEditView.coffee | 102 ++++++++++++++++++ .../levels/thangs/thang_type_handler.coffee | 1 + 6 files changed, 114 insertions(+), 6 deletions(-) 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/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/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) -> From bf9aa27e73d318f4bb2f1873d5c224d5b72171ea Mon Sep 17 00:00:00 2001 From: Nick Winter Date: Thu, 1 Jan 2015 14:59:55 -0800 Subject: [PATCH 28/28] Added tasks view to Thang editor search. --- app/locale/en.coffee | 1 + app/templates/editor/thang/table.jade | 9 ++++++++- app/views/common/SearchView.coffee | 2 +- app/views/editor/thang/ThangTypeSearchView.coffee | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/app/locale/en.coffee b/app/locale/en.coffee index 945ecc668..26d8f38cd 100644 --- a/app/locale/en.coffee +++ b/app/locale/en.coffee @@ -657,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/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/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/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: ->