mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-24 08:08:15 -05:00
Merge branch 'master' into production
This commit is contained in:
commit
3ba746094a
24 changed files with 370 additions and 210 deletions
|
@ -131,6 +131,18 @@ module.exports.kindaEqual = compare = (l, r) ->
|
|||
else
|
||||
return false
|
||||
|
||||
# Return UTC string "YYYY-MM-DD" for today + offset
|
||||
module.exports.getUTCDay = (offset=0) ->
|
||||
# TODO: Move to utility
|
||||
day = new Date()
|
||||
day.setDate(day.getUTCDate() + offset)
|
||||
partYear = day.getUTCFullYear()
|
||||
partMonth = (day.getUTCMonth() + 1)
|
||||
partMonth = "0" + partMonth if partMonth < 10
|
||||
partDay = day.getUTCDate()
|
||||
partDay = "0" + partDay if partDay < 10
|
||||
"#{partYear}-#{partMonth}-#{partDay}"
|
||||
|
||||
# Fast, basic way to replace text in an element when you don't need much.
|
||||
# http://stackoverflow.com/a/4962398/540620
|
||||
if document?
|
||||
|
|
|
@ -159,6 +159,10 @@
|
|||
date: "Date"
|
||||
body: "Body"
|
||||
version: "Version"
|
||||
pending: "Pending"
|
||||
accepted: "Accepted"
|
||||
rejected: "Rejected"
|
||||
withdrawn: "Withdrawn"
|
||||
submitter: "Submitter"
|
||||
submitted: "Submitted"
|
||||
commit_msg: "Commit Message"
|
||||
|
|
|
@ -145,13 +145,13 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
|
|||
fork: "Bifurcar"
|
||||
play: "Jugar" # When used as an action verb, like "Play next level"
|
||||
retry: "Reintentar"
|
||||
# actions: "Actions"
|
||||
actions: "Acciones"
|
||||
# info: "Info"
|
||||
# help: "Help"
|
||||
help: "Ayuda"
|
||||
watch: "Mirar"
|
||||
unwatch: "Pasar"
|
||||
submit_patch: "Mandar Parche"
|
||||
# submit_changes: "Submit Changes"
|
||||
submit_patch: "Enviar Parche"
|
||||
submit_changes: "Enviar Cambios"
|
||||
|
||||
general:
|
||||
and: "y"
|
||||
|
@ -165,10 +165,10 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
|
|||
# review: "Review"
|
||||
version_history: "Historial de versión"
|
||||
version_history_for: "Historial de las versiones de: "
|
||||
# select_changes: "Select two changes below to see the difference."
|
||||
# undo_prefix: "Undo"
|
||||
select_changes: "Selecciona dos cambios más abajo para ver la diferencia."
|
||||
undo_prefix: "Deshacer"
|
||||
# undo_shortcut: "(Ctrl+Z)"
|
||||
# redo_prefix: "Redo"
|
||||
redo_prefix: "Rehacer"
|
||||
# redo_shortcut: "(Ctrl+Shift+Z)"
|
||||
# play_preview: "Play preview of current level"
|
||||
result: "Resultado"
|
||||
|
@ -233,7 +233,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
|
|||
reload_title: "¿Recargar todo el código?"
|
||||
reload_really: "¿Estas seguro que quieres reiniciar el nivel?"
|
||||
reload_confirm: "Recargarlo todo"
|
||||
# victory: "Victory"
|
||||
victory: "Victoria"
|
||||
victory_title_prefix: "¡"
|
||||
victory_title_suffix: " Completado!"
|
||||
victory_sign_up: "Regístrate para recibir actualizaciones."
|
||||
|
@ -246,8 +246,8 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
|
|||
victory_review: "¡Cuéntanos más!" # Only in old-style levels.
|
||||
victory_hour_of_code_done: "¿Ya terminaste?"
|
||||
victory_hour_of_code_done_yes: "Si, ¡He terminado con mi hora de código!"
|
||||
# victory_experience_gained: "XP Gained"
|
||||
# victory_gems_gained: "Gems Gained"
|
||||
victory_experience_gained: "XP Conseguida"
|
||||
victory_gems_gained: "Gemas Conseguidas"
|
||||
guide_title: "Guía"
|
||||
tome_minion_spells: "Los hechizos de tus súbditos" # Only in old-style levels.
|
||||
tome_read_only_spells: "Hechizos de solo lectura" # Only in old-style levels.
|
||||
|
@ -271,7 +271,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
|
|||
loading_ready: "¡Listo!"
|
||||
loading_start: "Iniciar Nivel"
|
||||
problem_alert_title: "Arregla tu código"
|
||||
# problem_alert_help: "Help"
|
||||
problem_alert_help: "Ayuda"
|
||||
time_current: "Ahora:"
|
||||
time_total: "Máx:"
|
||||
time_goto: "Ir a:"
|
||||
|
@ -304,7 +304,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
|
|||
tip_talk_is_cheap: "Hablar es fácil. Enséñame el código. - Linus Torvalds"
|
||||
tip_first_language: "La cosa más desastrosa que puedes aprender es tu primer lenguaje de programación. - Alan Kay"
|
||||
tip_hardware_problem: "P: Cuantos programadores hacen falta para cambiar una bombilla? R: Ninguno, es un problema de hardware."
|
||||
# tip_hofstadters_law: "Hofstadter's Law: It always takes longer than you expect, even when you take into account Hofstadter's Law."
|
||||
tip_hofstadters_law: "Ley de Hofstadter: Siempre lleva más tiempo de lo que esperas, incluso cuando tienes en cuenta la Ley de Hofstadter."
|
||||
tip_premature_optimization: "La optimizacion prematura es la raiz de todo mal. - Donald Knuth"
|
||||
tip_brute_force: "Cuando haya dudas, usa la fuerza bruta. - Ken Thompson"
|
||||
tip_extrapolation: "Existen solo dos clases de personas: aquellos que pueden extrapolar desde información incompleta..."
|
||||
|
|
|
@ -68,7 +68,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
change_hero: "重新选择英雄" # Go back from choose inventory to choose hero
|
||||
choose_inventory: "装备道具"
|
||||
buy_gems: "购买宝石"
|
||||
# campaign_desert: "Desert Campaign"
|
||||
campaign_desert: "沙漠战役"
|
||||
campaign_forest: "森林战役"
|
||||
campaign_dungeon: "地牢战役"
|
||||
subscription_required: "需订阅"
|
||||
|
@ -106,9 +106,9 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
load_profile: "载入 G+ 档案"
|
||||
load_email: "载入 G+ 电子邮件"
|
||||
finishing: "完成..."
|
||||
# sign_in_with_facebook: "Sign in with Facebook"
|
||||
# sign_in_with_gplus: "Sign in with G+"
|
||||
# signup_switch: "Want to create an account?"
|
||||
sign_in_with_facebook: "Facebook账号登录"
|
||||
sign_in_with_gplus: " G+ 账号登录"
|
||||
signup_switch: "是否创建新账户?"
|
||||
|
||||
signup:
|
||||
email_announcements: "通过邮件接收通知"
|
||||
|
@ -117,7 +117,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
log_in: "登录"
|
||||
social_signup: "或者,你可以通过Facebook或G+注册:"
|
||||
required: "在做这件事情之前你必须先注册。"
|
||||
# login_switch: "Already have an account?"
|
||||
login_switch: "已经注册过账户?"
|
||||
|
||||
recover:
|
||||
recover_account_title: "找回账户"
|
||||
|
@ -145,13 +145,13 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
fork: "派生"
|
||||
play: "开始" # When used as an action verb, like "Play next level"
|
||||
retry: "重试"
|
||||
# actions: "Actions"
|
||||
# info: "Info"
|
||||
actions: "行为"
|
||||
info: "信息"
|
||||
help: "帮助"
|
||||
watch: "关注"
|
||||
unwatch: "取消关注"
|
||||
submit_patch: "提交补丁"
|
||||
# submit_changes: "Submit Changes"
|
||||
submit_changes: "提交更新"
|
||||
|
||||
general:
|
||||
and: "与"
|
||||
|
@ -159,18 +159,18 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
date: "日期"
|
||||
body: "正文"
|
||||
version: "版本"
|
||||
# submitter: "Submitter"
|
||||
# submitted: "Submitted"
|
||||
submitter: "提交者"
|
||||
submitted: "已提交"
|
||||
commit_msg: "提交信息"
|
||||
# review: "Review"
|
||||
review: "查看"
|
||||
version_history: "版本历史"
|
||||
version_history_for: "版本历史: "
|
||||
# select_changes: "Select two changes below to see the difference."
|
||||
# undo_prefix: "Undo"
|
||||
# undo_shortcut: "(Ctrl+Z)"
|
||||
# redo_prefix: "Redo"
|
||||
# redo_shortcut: "(Ctrl+Shift+Z)"
|
||||
# play_preview: "Play preview of current level"
|
||||
select_changes: "选择下面两项更新来查看其不同。"
|
||||
undo_prefix: "取消"
|
||||
undo_shortcut: "(Ctrl+Z)"
|
||||
redo_prefix: "重做"
|
||||
redo_shortcut: "(Ctrl+Shift+Z)"
|
||||
play_preview: "当前关卡预览"
|
||||
result: "结果"
|
||||
results: "结果"
|
||||
description: "描述"
|
||||
|
@ -233,7 +233,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
reload_title: "重载所有代码?"
|
||||
reload_really: "确定重载这一关,返回开始处吗?"
|
||||
reload_confirm: "重载所有"
|
||||
# victory: "Victory"
|
||||
victory: "胜利"
|
||||
victory_title_prefix: ""
|
||||
victory_title_suffix: " 完成"
|
||||
victory_sign_up: "保存进度"
|
||||
|
@ -246,8 +246,8 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
victory_review: "给我们反馈!" # Only in old-style levels.
|
||||
victory_hour_of_code_done: "你完成了吗?"
|
||||
victory_hour_of_code_done_yes: "是的, 完成了!"
|
||||
# victory_experience_gained: "XP Gained"
|
||||
# victory_gems_gained: "Gems Gained"
|
||||
victory_experience_gained: "获得经验"
|
||||
victory_gems_gained: "获得宝石"
|
||||
guide_title: "指南"
|
||||
tome_minion_spells: "助手的咒语" # Only in old-style levels.
|
||||
tome_read_only_spells: "只读的咒语" # Only in old-style levels.
|
||||
|
@ -271,7 +271,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
loading_ready: "载入完成!"
|
||||
loading_start: "开战"
|
||||
problem_alert_title: "修正你的代码"
|
||||
# problem_alert_help: "Help"
|
||||
problem_alert_help: "帮助"
|
||||
time_current: "现在:"
|
||||
time_total: "最大:"
|
||||
time_goto: "跳到:"
|
||||
|
@ -307,7 +307,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
tip_hofstadters_law: "侯世达定律:做事所花费的时间总是比你预期的要长,即使你的预期中考虑了侯世达定律。"
|
||||
tip_premature_optimization: "过早的优化是万恶之源。 - 高德纳"
|
||||
tip_brute_force: "拿不准时就用穷举法。 - Ken Thompson"
|
||||
# tip_extrapolation: "There are only two kinds of people: those that can extrapolate from incomplete data..."
|
||||
tip_extrapolation: "世界上只有两类人:一类人能够根据不完整的数据进行推断……"
|
||||
# tip_superpower: "Coding is the closest thing we have to a superpower."
|
||||
|
||||
game_menu:
|
||||
|
@ -315,8 +315,8 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
save_load_tab: "保存/打开"
|
||||
options_tab: "设置"
|
||||
guide_tab: "使用向导"
|
||||
# guide_video_tutorial: "Video Tutorial"
|
||||
# guide_tips: "Tips"
|
||||
guide_video_tutorial: "视频教程"
|
||||
guide_tips: "小技巧"
|
||||
multiplayer_tab: "多人游戏"
|
||||
auth_tab: "注册"
|
||||
inventory_caption: "装备你的英雄"
|
||||
|
@ -330,7 +330,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
inventory:
|
||||
choose_inventory: "装备道具"
|
||||
equipped_item: "已装备"
|
||||
# required_purchase_title: "Required"
|
||||
required_purchase_title: "需要"
|
||||
available_item: "可用"
|
||||
restricted_title: "被限制"
|
||||
should_equip: "(双击装备此道具)"
|
||||
|
@ -350,7 +350,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
prompt_title: "没有足够数量的宝石"
|
||||
prompt_body: "还需要更多吗?"
|
||||
prompt_button: "进入商店"
|
||||
# recovered: "Previous gems purchase recovered. Please refresh the page."
|
||||
recovered: "之前购买的宝石已恢复。请刷新页面。"
|
||||
|
||||
subscribe:
|
||||
subscribe_title: "订阅"
|
||||
|
@ -391,7 +391,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
regeneration: "恢复"
|
||||
range: "攻击范围" # As in "attack or visual range"
|
||||
blocks: "格挡" # As in "this shield blocks this much damage"
|
||||
# backstab: "Backstab" # As in "this dagger does this much backstab damage"
|
||||
backstab: "背刺" # As in "this dagger does this much backstab damage"
|
||||
skills: "技能"
|
||||
available_for_purchase: "可以购买" # Shows up when you have unlocked, but not purchased, a hero in the hero store
|
||||
level_to_unlock: "未解锁关卡:" # Label for which level you have to beat to unlock a particular hero (click a locked hero in the store to see)
|
||||
|
@ -574,10 +574,10 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
classes:
|
||||
archmage_title: "大法师"
|
||||
archmage_title_description: "(代码编写人员)"
|
||||
# archmage_summary: "If you are a developer interested in coding educational games, become an archmage to help us build CodeCombat!"
|
||||
archmage_summary: "如果你是对教育类游戏感兴趣的开发者,那么就选择大法师来帮我们为 CodeCombat编写代码吧!"
|
||||
artisan_title: "工匠"
|
||||
artisan_title_description: "(关卡建立人员)"
|
||||
# artisan_summary: "Build and share levels for you and your friends to play. Become an Artisan to learn the art of teaching others to program."
|
||||
artisan_summary: "建立游戏关卡并分享给朋友们。那么就选择工匠职业来教其他人编程吧。"
|
||||
adventurer_title: "冒险家"
|
||||
adventurer_title_description: "(关卡测试人员)"
|
||||
# adventurer_summary: "Get our new levels (even our subscriber content) for free one week early and help us work out bugs before our public release."
|
||||
|
@ -659,7 +659,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
# achievement_query_goals: "Key achievement off of level goals"
|
||||
level_completion: "关卡完成"
|
||||
pop_i18n: "填写 I18N"
|
||||
# tasks: "Tasks"
|
||||
tasks: "任务"
|
||||
|
||||
article:
|
||||
edit_btn_preview: "预览"
|
||||
|
@ -829,7 +829,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
# service_apple: "Apple"
|
||||
# service_web: "Web"
|
||||
# paid_on: "Paid On"
|
||||
# service: "Service"
|
||||
service: "服务"
|
||||
price: "价格"
|
||||
gems: "宝石"
|
||||
# active: "Active"
|
||||
|
@ -934,7 +934,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
practices_title: "尊重最佳实践"
|
||||
practices_description: "这是我们对您的承诺,即玩家,尽管这在法律用语中略显不足。"
|
||||
privacy_title: "隐私"
|
||||
# privacy_description: "We will not sell any of your personal information."
|
||||
privacy_description: "我们不会泄露您的个人信息。"
|
||||
security_title: "安全"
|
||||
security_description: "我们竭力保证您的个人信息安全性。作为一个开源项目,任何人都可以检讨并改善我们自由开放的网站的安全性。"
|
||||
email_title: "电子邮件"
|
||||
|
@ -1026,7 +1026,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
next_photo: "添加一张职业的照片(可选)。"
|
||||
next_active: "将自己标记为正在寻求工作机会以使自己的名字出现在搜索结果中。"
|
||||
example_blog: "你的博客"
|
||||
# example_personal_site: "Personal Site"
|
||||
example_personal_site: "个人主页"
|
||||
links_header: "个人网站链接"
|
||||
links_blurb: "链接任何你希望展示的网站或介绍,例如你的Github,你的领英档案,或是你的博客。"
|
||||
links_name: "链接名称"
|
||||
|
@ -1088,7 +1088,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
education_description: "描述"
|
||||
education_description_help: "展示任何与你的教育经历相关的信息。(140个字符;选填)"
|
||||
our_notes: "我们的评注"
|
||||
# remarks: "Remarks"
|
||||
remarks: "评价"
|
||||
projects: "项目"
|
||||
projects_header: "添加3个项目"
|
||||
projects_header_2: "项目(前3个)"
|
||||
|
@ -1101,7 +1101,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
project_picture_help: "上传一站230x115像素或更大的图片来展示这个项目。"
|
||||
project_link: "链接"
|
||||
project_link_help: "项目的链接。"
|
||||
# player_code: "Player Code"
|
||||
player_code: "玩家代码"
|
||||
|
||||
employers:
|
||||
# deprecation_warning_title: "Sorry, CodeCombat is not recruiting right now."
|
||||
|
@ -1165,6 +1165,6 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
|||
av_other_sub_title: "其他"
|
||||
av_other_debug_base_url: "Base(用于调试 base.jade)"
|
||||
u_title: "用户列表"
|
||||
# ucp_title: "User Code Problems"
|
||||
ucp_title: "用户代码的问题"
|
||||
lg_title: "最新的游戏"
|
||||
clas: "贡献者许可协议"
|
||||
|
|
|
@ -84,7 +84,7 @@ module.exports = class User extends CocoModel
|
|||
gemsEarned = @get('earned')?.gems ? 0
|
||||
gemsPurchased = @get('purchased')?.gems ? 0
|
||||
gemsSpent = @get('spent') ? 0
|
||||
gemsEarned + gemsPurchased - gemsSpent
|
||||
Math.floor gemsEarned + gemsPurchased - gemsSpent
|
||||
|
||||
heroes: ->
|
||||
heroes = (me.get('purchased')?.heroes ? []).concat([ThangType.heroes.captain, ThangType.heroes.knight])
|
||||
|
@ -139,7 +139,7 @@ module.exports = class User extends CocoModel
|
|||
else
|
||||
@subscribeCopyGroup = 'original'
|
||||
@subscribeCopyGroup
|
||||
|
||||
|
||||
getVideoTutorialStylesIndex: (numVideos=0)->
|
||||
# A/B Testing video tutorial styles
|
||||
# Not a constant number of videos available (e.g. could be 0, 1, 3, or 4 currently)
|
||||
|
|
|
@ -34,7 +34,7 @@ defaultTasks = [
|
|||
'Write a loading tip, if needed.'
|
||||
'Click the Populate i18n button.'
|
||||
|
||||
'Mark whether it requires a subscription (after adventurer week).'
|
||||
'Mark whether it requires a subscription.'
|
||||
'Release to everyone via MailChimp.'
|
||||
|
||||
'Check completion/engagement/problem analytics.'
|
||||
|
|
|
@ -227,7 +227,7 @@ me.RewardSchema = (descriptionFragment='earned by achievements') ->
|
|||
me.stringID(links: [{rel: 'db', href: '/db/thang.type/{($)}/version'}], title: 'Item ThangType', description: 'A reference to the earned item ThangType.', format: 'thang-type')
|
||||
levels: me.array {uniqueItems: true, description: "Levels #{descriptionFragment}."},
|
||||
me.stringID(links: [{rel: 'db', href: '/db/level/{($)}/version'}], title: 'Level', description: 'A reference to the earned Level.', format: 'latest-version-original-reference')
|
||||
gems: me.int {description: "Gems #{descriptionFragment}."}
|
||||
gems: me.float {description: "Gems #{descriptionFragment}."}
|
||||
|
||||
me.task = me.object {title: 'Task', description: 'A task to be completed', format: 'task', default: {name: 'TODO', complete: false}},
|
||||
name: {title: 'Name', description: 'What must be done?', type: 'string'}
|
||||
|
|
|
@ -30,7 +30,7 @@ block content
|
|||
|
||||
h4(data-i18n="contribute.how_to_join") How to Join
|
||||
p
|
||||
a(title='Contact', tabindex=-1, data-toggle="coco-modal", data-target="core/ContactModal", data-i18n="contribute.contact_us_url")
|
||||
a(tabindex=-1, data-toggle="coco-modal", data-target="core/ContactModal", data-i18n="contribute.contact_us_url")
|
||||
| Contact us
|
||||
span ,
|
||||
span(data-i18n="contribute.ambassador_join_desc")
|
||||
|
|
|
@ -41,13 +41,13 @@ block content
|
|||
p
|
||||
span(data-i18n="contribute.join_desc_1")
|
||||
| Anyone can help out! Just check out our
|
||||
a(title='GitHub', href="https://github.com/codecombat/codecombat", tabindex=-1)
|
||||
a(href="https://github.com/codecombat/codecombat", tabindex=-1)
|
||||
| GitHub
|
||||
span
|
||||
span(data-i18n="contribute.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?
|
||||
a(title='Contact', tabindex=-1, data-toggle="coco-modal", data-target="core/ContactModal", data-i18n="contribute.join_url_email")
|
||||
a(tabindex=-1, data-toggle="coco-modal", data-target="core/ContactModal", data-i18n="contribute.join_url_email")
|
||||
| Email us
|
||||
span(data-i18n="contribute.join_desc_3")
|
||||
| , or find us in our
|
||||
|
|
|
@ -36,7 +36,7 @@ block content
|
|||
|
||||
h4(data-i18n="contribute.how_to_join") How To Join
|
||||
p
|
||||
a(title='Contact', tabindex=-1, data-toggle="coco-modal", data-target="core/ContactModal", data-i18n="contribute.contact_us_url")
|
||||
a(tabindex=-1, data-toggle="coco-modal", data-target="core/ContactModal", data-i18n="contribute.contact_us_url")
|
||||
| Contact us
|
||||
span ,
|
||||
span(data-i18n="contribute.scribe_join_description")
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
|
||||
h4 Recent Sessions
|
||||
if recentSessions
|
||||
div(style='font-size:10pt') Latest 10 sessions for this level
|
||||
div(style='font-size:10pt') Latest #{recentSessions.length} sessions for this level
|
||||
div(style='font-size:10pt') Double-click row to open player and session
|
||||
table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt')
|
||||
thead
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
.btn-group(data-toggle="buttons").status-buttons
|
||||
label.btn.btn-default.pending
|
||||
input(type="radio", name="status", value="pending")
|
||||
input(type="radio", name="status", value="pending", data-i18n="general.pending")
|
||||
| Pending
|
||||
label.btn.btn-default.accepted
|
||||
input(type="radio", name="status", value="accepted")
|
||||
input(type="radio", name="status", value="accepted", data-i18n="general.accepted")
|
||||
| Accepted
|
||||
label.btn.btn-default.rejected
|
||||
input(type="radio", name="status", value="rejected")
|
||||
input(type="radio", name="status", value="rejected", data-i18n="general.rejected")
|
||||
| Rejected
|
||||
label.btn.btn-default.withdrawn
|
||||
input(type="radio", name="status", value="withdrawn")
|
||||
input(type="radio", name="status", value="withdrawn", data-i18n="general.withdrawn")
|
||||
| Withdrawn
|
||||
|
||||
if patches.loading
|
||||
|
|
|
@ -243,9 +243,7 @@ module.exports = class CampaignEditorView extends RootView
|
|||
getCampaignCompletions: =>
|
||||
# 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()
|
||||
startDay = utils.getUTCDay -6
|
||||
|
||||
success = (data) =>
|
||||
return if @destroyed
|
||||
|
|
|
@ -3,6 +3,7 @@ Level = require 'models/Level'
|
|||
LevelSession = require 'models/LevelSession'
|
||||
ModelModal = require 'views/modal/ModelModal'
|
||||
User = require 'models/User'
|
||||
utils = require 'core/utils'
|
||||
|
||||
module.exports = class CampaignLevelView extends CocoView
|
||||
id: 'campaign-level-view'
|
||||
|
@ -47,9 +48,7 @@ module.exports = class CampaignLevelView extends CocoView
|
|||
|
||||
getCommonLevelProblems: ->
|
||||
# Fetch last 30 days of common level problems
|
||||
startDay = new Date()
|
||||
startDay.setDate(startDay.getUTCDate() - 29)
|
||||
startDay = startDay.getUTCFullYear() + '-' + (startDay.getUTCMonth() + 1) + '-' + startDay.getUTCDate()
|
||||
startDay = utils.getUTCDay -29
|
||||
|
||||
success = (data) =>
|
||||
return if @destroyed
|
||||
|
@ -77,9 +76,7 @@ module.exports = class CampaignLevelView extends CocoView
|
|||
@levelCompletions = _.map data, mapFn, @
|
||||
@render()
|
||||
|
||||
startDay = new Date()
|
||||
startDay.setDate(startDay.getUTCDate() - 6)
|
||||
startDay = startDay.getUTCFullYear() + '-' + (startDay.getUTCMonth() + 1) + '-' + startDay.getUTCDate()
|
||||
startDay = utils.getUTCDay -6
|
||||
|
||||
# TODO: Why do we need this url dash?
|
||||
request = @supermodel.addRequestResource 'level_completions', {
|
||||
|
@ -97,10 +94,8 @@ module.exports = class CampaignLevelView extends CocoView
|
|||
@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()
|
||||
|
||||
startDay = utils.getUTCDay -6
|
||||
|
||||
# TODO: Why do we need this url dash?
|
||||
request = @supermodel.addRequestResource 'playtime_averages', {
|
||||
url: '/db/level/-/playtime_averages'
|
||||
|
@ -111,7 +106,7 @@ module.exports = class CampaignLevelView extends CocoView
|
|||
request.load()
|
||||
|
||||
getRecentSessions: ->
|
||||
limit = 10
|
||||
limit = 100
|
||||
|
||||
success = (data) =>
|
||||
return if @destroyed
|
||||
|
|
|
@ -110,6 +110,8 @@ module.exports = class HeroVictoryModal extends ModalView
|
|||
# rewards = achievement.get('rewards') or {}
|
||||
# rewards.gems *= (index + 1)
|
||||
|
||||
# TODO: use earned achievement worths or somehow pull in recalculated exp/gems
|
||||
|
||||
c.thangTypes = @thangTypes
|
||||
c.me = me
|
||||
c.readyToRank = @level.get('type', true) is 'hero-ladder' and @session.readyToRank()
|
||||
|
|
82
scripts/analytics/mixpanelLevelRates.py
Normal file
82
scripts/analytics/mixpanelLevelRates.py
Normal file
|
@ -0,0 +1,82 @@
|
|||
# Calculate level completion rates via mixpanel export API
|
||||
|
||||
# TODO: unique users
|
||||
# TODO: align output
|
||||
# TODO: order output
|
||||
|
||||
import sys
|
||||
from mixpanel import Mixpanel
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
# NOTE: mixpanel dates are by day and inclusive
|
||||
# E.g. '2014-12-08' is any date that day, up to 2014-12-09 12am
|
||||
|
||||
if __name__ == '__main__':
|
||||
if not len(sys.argv) is 3:
|
||||
print "Script format: <script> <api_key> <api_secret>"
|
||||
else:
|
||||
api_key = sys.argv[1]
|
||||
api_secret = sys.argv[2]
|
||||
api = Mixpanel(
|
||||
api_key = api_key,
|
||||
api_secret = api_secret
|
||||
)
|
||||
|
||||
startDate = '2014-12-31'
|
||||
endDate = '2015-01-05'
|
||||
print("Requesting data for {0} to {1}".format(startDate, endDate))
|
||||
data = api.request(['export'], {
|
||||
'event' : ['Started Level', 'Saw Victory'],
|
||||
'from_date' : startDate,
|
||||
'to_date' : endDate
|
||||
})
|
||||
|
||||
levelRates = {}
|
||||
lines = data.split('\n')
|
||||
print "Received %d entries" % len(lines)
|
||||
for line in lines:
|
||||
try:
|
||||
if len(line) is 0: continue
|
||||
eventData = json.loads(line)
|
||||
eventName = eventData['event']
|
||||
if not eventName in ['Started Level', 'Saw Victory']:
|
||||
print 'Unexpected event ' + eventName
|
||||
break
|
||||
properties = eventData['properties']
|
||||
if 'levelID' in properties:
|
||||
levelID = properties['levelID']
|
||||
elif 'level' in properties:
|
||||
levelID = properties['level'].lower().replace(' ', '-')
|
||||
else:
|
||||
print("Unkonwn levelID for", eventName)
|
||||
print(properties)
|
||||
break
|
||||
if not levelID in levelRates:
|
||||
levelRates[levelID] = {'started': 0, 'finished': 0}
|
||||
if eventName == 'Started Level':
|
||||
levelRates[levelID]['started'] += 1
|
||||
elif eventName == 'Saw Victory':
|
||||
levelRates[levelID]['finished'] += 1
|
||||
else:
|
||||
print("Unknown event name", eventName)
|
||||
print(eventData)
|
||||
break
|
||||
except:
|
||||
print "Unexpected error:", sys.exc_info()[0]
|
||||
print line
|
||||
break
|
||||
|
||||
# print(levelRates)
|
||||
for levelID in levelRates:
|
||||
started = levelRates[levelID]['started']
|
||||
finished = levelRates[levelID]['finished']
|
||||
# if not levelID == 'endangered-burl':
|
||||
# continue
|
||||
if started > 0:
|
||||
print("{0}\t{1}\t{2}\t{3}%".format(levelID, started, finished, float(finished) / started * 100))
|
||||
else:
|
||||
print("{0}\t{1}\t{2}".format(levelID, started, finished))
|
|
@ -20,12 +20,15 @@ print("Today is " + today);
|
|||
var todayMinus6 = new Date();
|
||||
todayMinus6.setDate(todayMinus6.getUTCDate() - 6);
|
||||
var startDate = todayMinus6.toISOString().substr(0, 10) + "T00:00:00.000Z";
|
||||
// startDate = "2014-12-01T00:00:00.000Z";
|
||||
// startDate = "2014-12-31T00:00:00.000Z";
|
||||
print("Start date is " + startDate)
|
||||
// var endDate = "2015-01-06T00:00:00.000Z";
|
||||
// print("End date is " + endDate)
|
||||
|
||||
var cursor = db['analytics.log.events'].find({
|
||||
$and: [
|
||||
{"created": { $gte: ISODate(startDate)}},
|
||||
// {"created": { $lt: ISODate(endDate)}},
|
||||
{$or: [ {"event" : 'Started Level'}, {"event" : 'Saw Victory'}]}
|
||||
]
|
||||
});
|
||||
|
@ -115,6 +118,7 @@ var campaigns = {
|
|||
// Bucketize events by user
|
||||
print("Getting event data...");
|
||||
var userProgression = {};
|
||||
var userLevelEventMap = {}; // Only want unique users per-level/event
|
||||
while (cursor.hasNext()) {
|
||||
var doc = cursor.next();
|
||||
var created = doc.created;
|
||||
|
@ -124,12 +128,17 @@ while (cursor.hasNext()) {
|
|||
if (level) {
|
||||
if (level.length > longestLevelName) longestLevelName = level.length;
|
||||
var user = doc.user.valueOf();
|
||||
if (!userProgression[user]) userProgression[user] = [];
|
||||
userProgression[user].push({
|
||||
created: created,
|
||||
event: event,
|
||||
level: level
|
||||
});
|
||||
if (!userLevelEventMap[user]) userLevelEventMap[user] = {};
|
||||
if (!userLevelEventMap[user][level]) userLevelEventMap[user][level] = {};
|
||||
if (!userLevelEventMap[user][level][event]) {
|
||||
userLevelEventMap[user][level][event] = true;
|
||||
if (!userProgression[user]) userProgression[user] = [];
|
||||
userProgression[user].push({
|
||||
created: created,
|
||||
event: event,
|
||||
level: level
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
longestLevelName += 2;
|
||||
|
@ -224,7 +233,7 @@ for (campaign in campaigns) {
|
|||
print("\n" + campaign);
|
||||
var level = "level";
|
||||
var levelSpacer = new Array(longestLevelName - level.length).join(' ');
|
||||
print(level + levelSpacer + "started\tdropped\t\tfinished dropped");
|
||||
print(level + levelSpacer + "started\tdropped\t\tfinished dropped\tcompletion");
|
||||
for (var i = 0; i < campaignRates[campaign].levels.length; i++) {
|
||||
var level = campaignRates[campaign].levels[i].level;
|
||||
var started = campaignRates[campaign].levels[i].started;
|
||||
|
@ -232,13 +241,13 @@ for (campaign in campaigns) {
|
|||
var finished = campaignRates[campaign].levels[i].finished;
|
||||
var finishDropped = campaignRates[campaign].levels[i].finishDropped;
|
||||
var levelSpacer = new Array(longestLevelName - level.length).join(' ');
|
||||
print(level + levelSpacer + started + "\t" + (started < 100 ? "\t" : "") + startDropped + "\t" + (startDropped / started * 100).toFixed(2) + "%\t" + finished + "\t" + finishDropped + "\t" + (finishDropped / finished * 100).toFixed(2) + "%");
|
||||
print(level + levelSpacer + started + "\t" + (started < 100 ? "\t" : "") + startDropped + "\t" + (startDropped / started * 100).toFixed(2) + "%\t" + finished + "\t" + finishDropped + "\t" + (finishDropped / finished * 100).toFixed(2) + "%" + "\t" + (finished / started * 100).toFixed(2) + "%");
|
||||
}
|
||||
var level = 'Overall';
|
||||
var started = campaignRates[campaign].overall.started;
|
||||
var startDropped = campaignRates[campaign].overall.startDropped;
|
||||
var finished = campaignRates[campaign].overall.finished;
|
||||
var finishDropped = campaignRates[campaign].overall.finishDropped;
|
||||
var levelSpacer = new Array(longestLevelName - level.length).join(' ');
|
||||
print(level + levelSpacer + started + "\t" + (started < 100 ? "\t" : "") + startDropped + "\t" + (startDropped / started * 100).toFixed(2) + "%\t" + finished + "\t" + finishDropped + "\t" + (finishDropped / finished * 100).toFixed(2) + "%");
|
||||
// var level = 'Overall';
|
||||
// var started = campaignRates[campaign].overall.started;
|
||||
// var startDropped = campaignRates[campaign].overall.startDropped;
|
||||
// var finished = campaignRates[campaign].overall.finished;
|
||||
// var finishDropped = campaignRates[campaign].overall.finishDropped;
|
||||
// var levelSpacer = new Array(longestLevelName - level.length).join(' ');
|
||||
// print(level + levelSpacer + started + "\t" + (started < 100 ? "\t" : "") + startDropped + "\t" + (startDropped / started * 100).toFixed(2) + "%\t" + finished + "\t" + finishDropped + "\t" + (finishDropped / finished * 100).toFixed(2) + "%");
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ EarnedAchievementSchema.pre 'save', (next) ->
|
|||
EarnedAchievementSchema.index({user: 1, achievement: 1}, {unique: true, name: 'earned achievement index'})
|
||||
EarnedAchievementSchema.index({user: 1, changed: -1}, {name: 'latest '})
|
||||
|
||||
EarnedAchievementSchema.statics.createForAchievement = (achievement, doc, originalDocObj, done) ->
|
||||
EarnedAchievementSchema.statics.createForAchievement = (achievement, doc, originalDocObj=null, previouslyEarnedAchievement=null, done) ->
|
||||
User = require '../users/User'
|
||||
userObjectID = doc.get(achievement.get('userField'))
|
||||
userID = if _.isObject userObjectID then userObjectID.toHexString() else userObjectID # Standardize! Use strings, not ObjectId's
|
||||
|
@ -27,18 +27,20 @@ EarnedAchievementSchema.statics.createForAchievement = (achievement, doc, origin
|
|||
achievementName: achievement.get 'name'
|
||||
earnedRewards: achievement.get 'rewards'
|
||||
|
||||
worth = achievement.get('worth') ? 10
|
||||
pointWorth = achievement.get('worth') ? 10
|
||||
gemWorth = achievement.get('rewards')?.gems ? 0
|
||||
earnedPoints = 0
|
||||
earnedGems = 0
|
||||
|
||||
wrapUp = (earnedAchievementDoc) ->
|
||||
# Update user's experience points
|
||||
update = {$inc: {points: earnedPoints}}
|
||||
update = {$inc: {points: earnedPoints, 'earned.gems': earnedGems}}
|
||||
for rewardType, rewards of achievement.get('rewards') ? {}
|
||||
if rewardType is 'gems'
|
||||
update.$inc['earned.gems'] = rewards if rewards
|
||||
else if rewards.length
|
||||
continue if rewardType is 'gems'
|
||||
if rewards.length
|
||||
update.$addToSet ?= {}
|
||||
update.$addToSet["earned.#{rewardType}"] = $each: rewards
|
||||
User.update {_id: userID}, update, {}, (err, count) ->
|
||||
User.update {_id: mongoose.Types.ObjectId(userID)}, update, {}, (err, count) ->
|
||||
log.error err if err?
|
||||
done?(earnedAchievementDoc)
|
||||
|
||||
|
@ -46,29 +48,40 @@ EarnedAchievementSchema.statics.createForAchievement = (achievement, doc, origin
|
|||
if isRepeatable
|
||||
#log.debug 'Upserting repeatable achievement called \'' + (achievement.get 'name') + '\' for ' + userID
|
||||
proportionalTo = achievement.get 'proportionalTo'
|
||||
originalAmount = if originalDocObj then util.getByPath(originalDocObj, proportionalTo) or 0 else 0
|
||||
docObj = doc.toObject()
|
||||
newAmount = docObj[proportionalTo]
|
||||
newAmount = util.getByPath(docObj, proportionalTo) or 0
|
||||
if previouslyEarnedAchievement
|
||||
originalAmount = previouslyEarnedAchievement.get('achievedAmount') or 0
|
||||
else if originalDocObj # This branch could get buggy if unchangedCopy tracking isn't working.
|
||||
originalAmount = util.getByPath(originalDocObj, proportionalTo) or 0
|
||||
else
|
||||
originalAmount = 0
|
||||
#console.log 'original amount is', originalAmount, 'and new amount is', newAmount, 'for', proportionalTo, 'with doc', docObj, 'and previously earned achievement amount', previouslyEarnedAchievement?.get('achievedAmount'), 'because we had originalDocObj', originalDocObj
|
||||
|
||||
if originalAmount isnt newAmount
|
||||
expFunction = achievement.getExpFunction()
|
||||
earned.notified = false
|
||||
earned.achievedAmount = newAmount
|
||||
earned.earnedPoints = (expFunction(newAmount) - expFunction(originalAmount)) * worth
|
||||
#console.log 'earnedPoints is', (expFunction(newAmount) - expFunction(originalAmount)) * pointWorth, 'was', earned.earnedPoints, earned.previouslyAchievedAmount, 'got exp function for new amount', newAmount, expFunction(newAmount), 'for original amount', originalAmount, expFunction(originalAmount), 'with point worth', pointWorth
|
||||
earnedPoints = earned.earnedPoints = (expFunction(newAmount) - expFunction(originalAmount)) * pointWorth
|
||||
earnedGems = earned.earnedGems = (expFunction(newAmount) - expFunction(originalAmount)) * gemWorth
|
||||
earned.previouslyAchievedAmount = originalAmount
|
||||
EarnedAchievement.update {achievement: earned.achievement, user: earned.user}, earned, {upsert: true}, (err) ->
|
||||
return log.debug err if err?
|
||||
|
||||
earnedPoints = earned.earnedPoints
|
||||
#log.debug earnedPoints
|
||||
wrapUp()
|
||||
wrapUp(new EarnedAchievement(earned))
|
||||
else
|
||||
done?()
|
||||
|
||||
else # not alreadyAchieved
|
||||
#log.debug 'Creating a new earned achievement called \'' + (achievement.get 'name') + '\' for ' + userID
|
||||
earned.earnedPoints = worth
|
||||
earned.earnedPoints = pointWorth
|
||||
earned.earnedGems = gemWorth
|
||||
(new EarnedAchievement(earned)).save (err, doc) ->
|
||||
return log.error err if err?
|
||||
earnedPoints = worth
|
||||
earnedPoints = pointWorth
|
||||
earnedGems = gemWorth
|
||||
wrapUp(doc)
|
||||
|
||||
User.saveActiveUser userID, "achievement"
|
||||
|
|
|
@ -6,6 +6,7 @@ EarnedAchievement = require './EarnedAchievement'
|
|||
User = require '../users/User'
|
||||
Handler = require '../commons/Handler'
|
||||
LocalMongo = require '../../app/lib/LocalMongo'
|
||||
util = require '../../app/core/utils'
|
||||
|
||||
class EarnedAchievementHandler extends Handler
|
||||
modelClass: EarnedAchievement
|
||||
|
@ -65,6 +66,10 @@ class EarnedAchievementHandler extends Handler
|
|||
return @sendNotFoundError(res, 'Could not find achievement.')
|
||||
else if not trigger
|
||||
return @sendNotFoundError(res, 'Could not find trigger.')
|
||||
else if achievement.get('proportionalTo') and earned
|
||||
EarnedAchievement.createForAchievement(achievement, trigger, null, earned, (earnedAchievementDoc) =>
|
||||
@sendCreated(res, (earnedAchievementDoc or earned)?.toObject())
|
||||
)
|
||||
else if earned
|
||||
achievementEarned = achievement.get('rewards')
|
||||
actuallyEarned = earned.get('earnedRewards')
|
||||
|
@ -82,10 +87,8 @@ class EarnedAchievementHandler extends Handler
|
|||
return @sendDatabaseError(res, err) if err
|
||||
return @sendSuccess(res, earned.toObject())
|
||||
)
|
||||
else if achievement.get('proportionalTo')
|
||||
return @sendBadInputError(res, 'Cannot currently do this to repeatable docs...')
|
||||
else
|
||||
EarnedAchievement.createForAchievement(achievement, trigger, null, (earnedAchievementDoc) =>
|
||||
EarnedAchievement.createForAchievement(achievement, trigger, null, null, (earnedAchievementDoc) =>
|
||||
@sendCreated(res, earnedAchievementDoc.toObject())
|
||||
)
|
||||
)
|
||||
|
@ -221,13 +224,15 @@ class EarnedAchievementHandler extends Handler
|
|||
notified: achievement._id in alreadyEarnedIDs
|
||||
|
||||
if isRepeatable
|
||||
earned.achievedAmount = something.get(achievement.get 'proportionalTo')
|
||||
earned.achievedAmount = util.getByPath(something.toObject(), achievement.get 'proportionalTo') or 0
|
||||
earned.previouslyAchievedAmount = 0
|
||||
|
||||
expFunction = achievement.getExpFunction()
|
||||
newPoints = expFunction(earned.achievedAmount) * achievement.get('worth') ? 10
|
||||
newGems = expFunction(earned.achievedAmount) * (achievement.get('rewards')?.gems ? 0)
|
||||
else
|
||||
newPoints = achievement.get('worth') ? 10
|
||||
newGems = achievement.get('rewards')?.gems ? 0
|
||||
|
||||
earned.earnedPoints = newPoints
|
||||
newTotalPoints += newPoints
|
||||
|
@ -235,7 +240,10 @@ class EarnedAchievementHandler extends Handler
|
|||
earned.earnedRewards = achievement.get('rewards')
|
||||
for rewardType in ['heroes', 'items', 'levels']
|
||||
newTotalRewards[rewardType] = newTotalRewards[rewardType].concat(achievement.get('rewards')?[rewardType] ? [])
|
||||
newTotalRewards.gems += achievement.get('rewards')?.gems ? 0
|
||||
if isRepeatable and earned.earnedRewards
|
||||
earned.earnedRewards = _.clone earned.earnedRewards
|
||||
earned.earnedRewards.gems = newGems
|
||||
newTotalRewards.gems += newGems
|
||||
|
||||
EarnedAchievement.update {achievement:earned.achievement, user:earned.user}, earned, {upsert: true}, (err) ->
|
||||
doneWithAchievement err
|
||||
|
|
|
@ -33,6 +33,7 @@ class AnalyticsLogEventHandler extends Handler
|
|||
# endDay - Exclusive, optional, e.g. '2014-12-16'
|
||||
|
||||
# TODO: An uncached call takes about 15s locally
|
||||
# TODO: Use unique users
|
||||
|
||||
levelSlug = req.query.slug or req.body.slug
|
||||
startDay = req.query.startDay or req.body.startDay
|
||||
|
@ -105,11 +106,12 @@ class AnalyticsLogEventHandler extends Handler
|
|||
# startDay - Inclusive, optional, e.g. '2014-12-14'
|
||||
# endDay - Exclusive, optional, e.g. '2014-12-16'
|
||||
|
||||
# TODO: Must be a better way to organize this series of 3 database calls (campaigns, levels, analytics)
|
||||
# TODO: Must be a better way to organize this series of database calls (campaigns, levels, analytics)
|
||||
# TODO: An uncached call can take over 30s locally
|
||||
# TODO: Returns all the campaigns
|
||||
# TODO: Calculate overall campaign stats
|
||||
# TODO: Assumes db campaign levels are in progression order. Should build this based on actual progression.
|
||||
# TODO: Remove earliest duplicate event so our dropped counts will be more accurate.
|
||||
|
||||
campaignSlug = req.query.slug or req.body.slug
|
||||
startDay = req.query.startDay or req.body.startDay
|
||||
|
@ -128,95 +130,111 @@ class AnalyticsLogEventHandler extends Handler
|
|||
cacheKey += 'e' + endDay if endDay?
|
||||
return @sendSuccess res, campaignDropOffs if campaignDropOffs = @campaignDropOffsCache[cacheKey]
|
||||
|
||||
getCompletions = (campaigns) =>
|
||||
getCompletions = (campaigns, userProgression) =>
|
||||
# Calculate campaign drop off rates
|
||||
# Input:
|
||||
# campaigns - per-campaign dictionary of ordered level slugs
|
||||
# userProgression - per-user event lists
|
||||
|
||||
# Remove duplicate user events
|
||||
for user of userProgression
|
||||
userProgression[user] = _.uniq userProgression[user], false, (val, index, arr) -> val.event + val.level
|
||||
|
||||
# Order user progression by created
|
||||
for user of 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
|
||||
completions = {}
|
||||
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
|
||||
completions[campaign] ?=
|
||||
levels: []
|
||||
# overall:
|
||||
# started: 0,
|
||||
# startDropped: 0,
|
||||
# finished: 0,
|
||||
# finishDropped: 0
|
||||
completions[campaign].levels.push
|
||||
level: level
|
||||
started: started
|
||||
startDropped: startDropped
|
||||
finished: finished
|
||||
finishDropped: finishDropped
|
||||
break
|
||||
|
||||
# Sort level data by campaign order
|
||||
for campaign of completions
|
||||
completions[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] = completions
|
||||
for campaign of completions
|
||||
cacheKey = campaign
|
||||
cacheKey += 's' + startDay if startDay?
|
||||
cacheKey += 'e' + endDay if endDay?
|
||||
@campaignDropOffsCache[cacheKey] = completions
|
||||
@sendSuccess res, completions
|
||||
|
||||
getUserEventData = (campaigns) =>
|
||||
# Gather user start and finish event data
|
||||
# Input:
|
||||
# campaigns - per-campaign dictionary of ordered level slugs
|
||||
# Output:
|
||||
# userProgression - per-user event lists
|
||||
|
||||
userProgression = {}
|
||||
|
||||
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
|
||||
completions = {}
|
||||
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
|
||||
completions[campaign] ?=
|
||||
levels: []
|
||||
# overall:
|
||||
# started: 0,
|
||||
# startDropped: 0,
|
||||
# finished: 0,
|
||||
# finishDropped: 0
|
||||
completions[campaign].levels.push
|
||||
level: level
|
||||
started: started
|
||||
startDropped: startDropped
|
||||
finished: finished
|
||||
finishDropped: finishDropped
|
||||
break
|
||||
|
||||
# Sort level data by campaign order
|
||||
for campaign of completions
|
||||
completions[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] = completions
|
||||
for campaign of completions
|
||||
cacheKey = campaign
|
||||
cacheKey += 's' + startDay if startDay?
|
||||
cacheKey += 'e' + endDay if endDay?
|
||||
@campaignDropOffsCache[cacheKey] = completions
|
||||
@sendSuccess res, completions
|
||||
# Query stream is better for large results
|
||||
# http://mongoosejs.com/docs/api.html#query_Query-stream
|
||||
stream = AnalyticsLogEvent.find(queryParams).select('created event properties user').stream()
|
||||
stream.on 'data', (item) =>
|
||||
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')
|
||||
return unless level?
|
||||
user = item.get('user')
|
||||
userProgression[user] ?= []
|
||||
userProgression[user].push
|
||||
created: created
|
||||
event: event
|
||||
level: level
|
||||
.on 'error', (err) =>
|
||||
return @sendDatabaseError res, err
|
||||
.on 'close', () =>
|
||||
getCompletions campaigns, userProgression
|
||||
|
||||
getLevelData = (campaigns, campaignLevelIDs) =>
|
||||
# Get level data and replace levelIDs with level slugs in campaigns
|
||||
|
@ -240,7 +258,7 @@ class AnalyticsLogEventHandler extends Handler
|
|||
mapFn = (item) -> levelSlugMap[item]
|
||||
campaigns[campaign] = _.map campaigns[campaign], mapFn, @
|
||||
|
||||
getCompletions campaigns
|
||||
getUserEventData campaigns
|
||||
|
||||
getCampaignData = () =>
|
||||
# Get campaign data
|
||||
|
|
|
@ -39,7 +39,7 @@ CampaignHandler = class CampaignHandler extends Handler
|
|||
|
||||
getRelatedLevels: (req, res, campaign, projection) ->
|
||||
extraProjectionProps = []
|
||||
if projection
|
||||
unless _.isEmpty(projection)
|
||||
# Make sure that permissions and version are fetched, but not sent back if they didn't ask for them.
|
||||
extraProjectionProps.push 'permissions' unless projection.permissions
|
||||
extraProjectionProps.push 'version' unless projection.version
|
||||
|
|
|
@ -14,7 +14,8 @@ AchievablePlugin = (schema, options) ->
|
|||
|
||||
# Keep track the document before it's saved
|
||||
schema.post 'init', (doc) ->
|
||||
doc.unchangedCopy = doc.toObject()
|
||||
unless doc.unchangedCopy
|
||||
doc.unchangedCopy = doc.toObject()
|
||||
|
||||
# Check if an achievement has been earned
|
||||
schema.post 'save', (doc) ->
|
||||
|
|
|
@ -575,8 +575,8 @@ sendLadderUpdateEmail = (session, now, daysAgo) ->
|
|||
unless user.get('email') and allowNotes and not session.unsubscribed
|
||||
#log.info "Not sending email to #{user.get('email')} #{user.get('name')} because they only want emails about #{user.get('emailSubscriptions')}, #{user.get('emails')} - session unsubscribed: #{session.unsubscribed}"
|
||||
return
|
||||
unless session.levelName
|
||||
#log.info "Not sending email to #{user.get('email')} #{user.get('name')} because the session had no levelName in it."
|
||||
unless session.levelName and session.team
|
||||
#log.info "Not sending email to #{user.get('email')} #{user.get('name')} because the session had levelName #{session.levelName} or team #{session.team} in it."
|
||||
return
|
||||
name = if user.get('firstName') and user.get('lastName') then "#{user.get('firstName')}" else user.get('name')
|
||||
name = 'Wizard' if not name or name is 'Anoner'
|
||||
|
|
|
@ -21,6 +21,8 @@ repeatable =
|
|||
userField: '_id'
|
||||
proportionalTo: 'simulatedBy'
|
||||
recalculable: true
|
||||
rewards:
|
||||
gems: 1
|
||||
|
||||
diminishing =
|
||||
name: 'Simulated2'
|
||||
|
@ -169,19 +171,23 @@ describe 'Achieving Achievements', ->
|
|||
|
||||
it 'verify that a repeatable achievement has been earned', (done) ->
|
||||
unittest.getNormalJoe (joe) ->
|
||||
EarnedAchievement.find {achievementName: repeatable.name}, (err, docs) ->
|
||||
expect(err).toBeNull()
|
||||
expect(docs.length).toBe(1)
|
||||
achievement = docs[0]
|
||||
|
||||
if achievement
|
||||
expect(achievement.get 'achievement').toBe repeatable._id
|
||||
expect(achievement.get 'user').toBe joe._id.toHexString()
|
||||
expect(achievement.get 'notified').toBeFalsy()
|
||||
expect(achievement.get 'earnedPoints').toBe 2 * repeatable.worth
|
||||
expect(achievement.get 'achievedAmount').toBe 2
|
||||
expect(achievement.get 'previouslyAchievedAmount').toBeFalsy()
|
||||
done()
|
||||
User.findById(joe.get('_id')).exec (err, joe2) ->
|
||||
expect(joe2.get('earned').gems).toBe(2)
|
||||
|
||||
EarnedAchievement.find {achievementName: repeatable.name}, (err, docs) ->
|
||||
expect(err).toBeNull()
|
||||
expect(docs.length).toBe(1)
|
||||
achievement = docs[0]
|
||||
|
||||
if achievement
|
||||
expect(achievement.get 'achievement').toBe repeatable._id
|
||||
expect(achievement.get 'user').toBe joe._id.toHexString()
|
||||
expect(achievement.get 'notified').toBeFalsy()
|
||||
expect(achievement.get 'earnedPoints').toBe 2 * repeatable.worth
|
||||
expect(achievement.get 'achievedAmount').toBe 2
|
||||
expect(achievement.get 'previouslyAchievedAmount').toBeFalsy()
|
||||
done()
|
||||
|
||||
it 'verify that the repeatable achievement with complex exp has been earned', (done) ->
|
||||
unittest.getNormalJoe (joe) ->
|
||||
|
@ -196,6 +202,17 @@ describe 'Achieving Achievements', ->
|
|||
|
||||
done()
|
||||
|
||||
it 'increases gems proportionally to changes made', (done) ->
|
||||
unittest.getNormalJoe (joe) ->
|
||||
User.findById(joe.get('_id')).exec (err, joe2) ->
|
||||
joe2.set('simulatedBy', 4)
|
||||
joe2.save (err, joe3) ->
|
||||
expect(err).toBeNull()
|
||||
User.findById(joe3.get('_id')).exec (err, joe4) ->
|
||||
expect(joe4.get('earned').gems).toBe(4) # Crap, it's 6 TODO
|
||||
done()
|
||||
|
||||
|
||||
describe 'Recalculate Achievements', ->
|
||||
EarnedAchievementHandler = require '../../../server/achievements/earned_achievement_handler'
|
||||
|
||||
|
@ -241,7 +258,8 @@ describe 'Recalculate Achievements', ->
|
|||
unittest.getNormalJoe (joe) ->
|
||||
User.findById joe.get('id'), (err, guy) ->
|
||||
expect(err).toBeNull()
|
||||
expect(guy.get 'points').toBe unlockable.worth + 2 * repeatable.worth + (Math.log(.5 * (2 + .5)) + 1) * diminishing.worth
|
||||
expect(guy.get 'points').toBe unlockable.worth + 4 * repeatable.worth + (Math.log(.5 * (4 + .5)) + 1) * diminishing.worth
|
||||
expect(guy.get('earned').gems).toBe 4 * repeatable.rewards.gems
|
||||
done()
|
||||
|
||||
it 'cleaning up test: deleting all Achievements and related', (done) ->
|
||||
|
|
Loading…
Reference in a new issue