mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-28 10:06:08 -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
|
else
|
||||||
return false
|
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.
|
# Fast, basic way to replace text in an element when you don't need much.
|
||||||
# http://stackoverflow.com/a/4962398/540620
|
# http://stackoverflow.com/a/4962398/540620
|
||||||
if document?
|
if document?
|
||||||
|
|
|
@ -159,6 +159,10 @@
|
||||||
date: "Date"
|
date: "Date"
|
||||||
body: "Body"
|
body: "Body"
|
||||||
version: "Version"
|
version: "Version"
|
||||||
|
pending: "Pending"
|
||||||
|
accepted: "Accepted"
|
||||||
|
rejected: "Rejected"
|
||||||
|
withdrawn: "Withdrawn"
|
||||||
submitter: "Submitter"
|
submitter: "Submitter"
|
||||||
submitted: "Submitted"
|
submitted: "Submitted"
|
||||||
commit_msg: "Commit Message"
|
commit_msg: "Commit Message"
|
||||||
|
|
|
@ -145,13 +145,13 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
|
||||||
fork: "Bifurcar"
|
fork: "Bifurcar"
|
||||||
play: "Jugar" # When used as an action verb, like "Play next level"
|
play: "Jugar" # When used as an action verb, like "Play next level"
|
||||||
retry: "Reintentar"
|
retry: "Reintentar"
|
||||||
# actions: "Actions"
|
actions: "Acciones"
|
||||||
# info: "Info"
|
# info: "Info"
|
||||||
# help: "Help"
|
help: "Ayuda"
|
||||||
watch: "Mirar"
|
watch: "Mirar"
|
||||||
unwatch: "Pasar"
|
unwatch: "Pasar"
|
||||||
submit_patch: "Mandar Parche"
|
submit_patch: "Enviar Parche"
|
||||||
# submit_changes: "Submit Changes"
|
submit_changes: "Enviar Cambios"
|
||||||
|
|
||||||
general:
|
general:
|
||||||
and: "y"
|
and: "y"
|
||||||
|
@ -165,10 +165,10 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
|
||||||
# review: "Review"
|
# review: "Review"
|
||||||
version_history: "Historial de versión"
|
version_history: "Historial de versión"
|
||||||
version_history_for: "Historial de las versiones de: "
|
version_history_for: "Historial de las versiones de: "
|
||||||
# select_changes: "Select two changes below to see the difference."
|
select_changes: "Selecciona dos cambios más abajo para ver la diferencia."
|
||||||
# undo_prefix: "Undo"
|
undo_prefix: "Deshacer"
|
||||||
# undo_shortcut: "(Ctrl+Z)"
|
# undo_shortcut: "(Ctrl+Z)"
|
||||||
# redo_prefix: "Redo"
|
redo_prefix: "Rehacer"
|
||||||
# redo_shortcut: "(Ctrl+Shift+Z)"
|
# redo_shortcut: "(Ctrl+Shift+Z)"
|
||||||
# play_preview: "Play preview of current level"
|
# play_preview: "Play preview of current level"
|
||||||
result: "Resultado"
|
result: "Resultado"
|
||||||
|
@ -233,7 +233,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
|
||||||
reload_title: "¿Recargar todo el código?"
|
reload_title: "¿Recargar todo el código?"
|
||||||
reload_really: "¿Estas seguro que quieres reiniciar el nivel?"
|
reload_really: "¿Estas seguro que quieres reiniciar el nivel?"
|
||||||
reload_confirm: "Recargarlo todo"
|
reload_confirm: "Recargarlo todo"
|
||||||
# victory: "Victory"
|
victory: "Victoria"
|
||||||
victory_title_prefix: "¡"
|
victory_title_prefix: "¡"
|
||||||
victory_title_suffix: " Completado!"
|
victory_title_suffix: " Completado!"
|
||||||
victory_sign_up: "Regístrate para recibir actualizaciones."
|
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_review: "¡Cuéntanos más!" # Only in old-style levels.
|
||||||
victory_hour_of_code_done: "¿Ya terminaste?"
|
victory_hour_of_code_done: "¿Ya terminaste?"
|
||||||
victory_hour_of_code_done_yes: "Si, ¡He terminado con mi hora de código!"
|
victory_hour_of_code_done_yes: "Si, ¡He terminado con mi hora de código!"
|
||||||
# victory_experience_gained: "XP Gained"
|
victory_experience_gained: "XP Conseguida"
|
||||||
# victory_gems_gained: "Gems Gained"
|
victory_gems_gained: "Gemas Conseguidas"
|
||||||
guide_title: "Guía"
|
guide_title: "Guía"
|
||||||
tome_minion_spells: "Los hechizos de tus súbditos" # Only in old-style levels.
|
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.
|
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_ready: "¡Listo!"
|
||||||
loading_start: "Iniciar Nivel"
|
loading_start: "Iniciar Nivel"
|
||||||
problem_alert_title: "Arregla tu código"
|
problem_alert_title: "Arregla tu código"
|
||||||
# problem_alert_help: "Help"
|
problem_alert_help: "Ayuda"
|
||||||
time_current: "Ahora:"
|
time_current: "Ahora:"
|
||||||
time_total: "Máx:"
|
time_total: "Máx:"
|
||||||
time_goto: "Ir a:"
|
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_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_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_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_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_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..."
|
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
|
change_hero: "重新选择英雄" # Go back from choose inventory to choose hero
|
||||||
choose_inventory: "装备道具"
|
choose_inventory: "装备道具"
|
||||||
buy_gems: "购买宝石"
|
buy_gems: "购买宝石"
|
||||||
# campaign_desert: "Desert Campaign"
|
campaign_desert: "沙漠战役"
|
||||||
campaign_forest: "森林战役"
|
campaign_forest: "森林战役"
|
||||||
campaign_dungeon: "地牢战役"
|
campaign_dungeon: "地牢战役"
|
||||||
subscription_required: "需订阅"
|
subscription_required: "需订阅"
|
||||||
|
@ -106,9 +106,9 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
||||||
load_profile: "载入 G+ 档案"
|
load_profile: "载入 G+ 档案"
|
||||||
load_email: "载入 G+ 电子邮件"
|
load_email: "载入 G+ 电子邮件"
|
||||||
finishing: "完成..."
|
finishing: "完成..."
|
||||||
# sign_in_with_facebook: "Sign in with Facebook"
|
sign_in_with_facebook: "Facebook账号登录"
|
||||||
# sign_in_with_gplus: "Sign in with G+"
|
sign_in_with_gplus: " G+ 账号登录"
|
||||||
# signup_switch: "Want to create an account?"
|
signup_switch: "是否创建新账户?"
|
||||||
|
|
||||||
signup:
|
signup:
|
||||||
email_announcements: "通过邮件接收通知"
|
email_announcements: "通过邮件接收通知"
|
||||||
|
@ -117,7 +117,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
||||||
log_in: "登录"
|
log_in: "登录"
|
||||||
social_signup: "或者,你可以通过Facebook或G+注册:"
|
social_signup: "或者,你可以通过Facebook或G+注册:"
|
||||||
required: "在做这件事情之前你必须先注册。"
|
required: "在做这件事情之前你必须先注册。"
|
||||||
# login_switch: "Already have an account?"
|
login_switch: "已经注册过账户?"
|
||||||
|
|
||||||
recover:
|
recover:
|
||||||
recover_account_title: "找回账户"
|
recover_account_title: "找回账户"
|
||||||
|
@ -145,13 +145,13 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
||||||
fork: "派生"
|
fork: "派生"
|
||||||
play: "开始" # When used as an action verb, like "Play next level"
|
play: "开始" # When used as an action verb, like "Play next level"
|
||||||
retry: "重试"
|
retry: "重试"
|
||||||
# actions: "Actions"
|
actions: "行为"
|
||||||
# info: "Info"
|
info: "信息"
|
||||||
help: "帮助"
|
help: "帮助"
|
||||||
watch: "关注"
|
watch: "关注"
|
||||||
unwatch: "取消关注"
|
unwatch: "取消关注"
|
||||||
submit_patch: "提交补丁"
|
submit_patch: "提交补丁"
|
||||||
# submit_changes: "Submit Changes"
|
submit_changes: "提交更新"
|
||||||
|
|
||||||
general:
|
general:
|
||||||
and: "与"
|
and: "与"
|
||||||
|
@ -159,18 +159,18 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
||||||
date: "日期"
|
date: "日期"
|
||||||
body: "正文"
|
body: "正文"
|
||||||
version: "版本"
|
version: "版本"
|
||||||
# submitter: "Submitter"
|
submitter: "提交者"
|
||||||
# submitted: "Submitted"
|
submitted: "已提交"
|
||||||
commit_msg: "提交信息"
|
commit_msg: "提交信息"
|
||||||
# review: "Review"
|
review: "查看"
|
||||||
version_history: "版本历史"
|
version_history: "版本历史"
|
||||||
version_history_for: "版本历史: "
|
version_history_for: "版本历史: "
|
||||||
# select_changes: "Select two changes below to see the difference."
|
select_changes: "选择下面两项更新来查看其不同。"
|
||||||
# undo_prefix: "Undo"
|
undo_prefix: "取消"
|
||||||
# undo_shortcut: "(Ctrl+Z)"
|
undo_shortcut: "(Ctrl+Z)"
|
||||||
# redo_prefix: "Redo"
|
redo_prefix: "重做"
|
||||||
# redo_shortcut: "(Ctrl+Shift+Z)"
|
redo_shortcut: "(Ctrl+Shift+Z)"
|
||||||
# play_preview: "Play preview of current level"
|
play_preview: "当前关卡预览"
|
||||||
result: "结果"
|
result: "结果"
|
||||||
results: "结果"
|
results: "结果"
|
||||||
description: "描述"
|
description: "描述"
|
||||||
|
@ -233,7 +233,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
||||||
reload_title: "重载所有代码?"
|
reload_title: "重载所有代码?"
|
||||||
reload_really: "确定重载这一关,返回开始处吗?"
|
reload_really: "确定重载这一关,返回开始处吗?"
|
||||||
reload_confirm: "重载所有"
|
reload_confirm: "重载所有"
|
||||||
# victory: "Victory"
|
victory: "胜利"
|
||||||
victory_title_prefix: ""
|
victory_title_prefix: ""
|
||||||
victory_title_suffix: " 完成"
|
victory_title_suffix: " 完成"
|
||||||
victory_sign_up: "保存进度"
|
victory_sign_up: "保存进度"
|
||||||
|
@ -246,8 +246,8 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
||||||
victory_review: "给我们反馈!" # Only in old-style levels.
|
victory_review: "给我们反馈!" # Only in old-style levels.
|
||||||
victory_hour_of_code_done: "你完成了吗?"
|
victory_hour_of_code_done: "你完成了吗?"
|
||||||
victory_hour_of_code_done_yes: "是的, 完成了!"
|
victory_hour_of_code_done_yes: "是的, 完成了!"
|
||||||
# victory_experience_gained: "XP Gained"
|
victory_experience_gained: "获得经验"
|
||||||
# victory_gems_gained: "Gems Gained"
|
victory_gems_gained: "获得宝石"
|
||||||
guide_title: "指南"
|
guide_title: "指南"
|
||||||
tome_minion_spells: "助手的咒语" # Only in old-style levels.
|
tome_minion_spells: "助手的咒语" # Only in old-style levels.
|
||||||
tome_read_only_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_ready: "载入完成!"
|
||||||
loading_start: "开战"
|
loading_start: "开战"
|
||||||
problem_alert_title: "修正你的代码"
|
problem_alert_title: "修正你的代码"
|
||||||
# problem_alert_help: "Help"
|
problem_alert_help: "帮助"
|
||||||
time_current: "现在:"
|
time_current: "现在:"
|
||||||
time_total: "最大:"
|
time_total: "最大:"
|
||||||
time_goto: "跳到:"
|
time_goto: "跳到:"
|
||||||
|
@ -307,7 +307,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
||||||
tip_hofstadters_law: "侯世达定律:做事所花费的时间总是比你预期的要长,即使你的预期中考虑了侯世达定律。"
|
tip_hofstadters_law: "侯世达定律:做事所花费的时间总是比你预期的要长,即使你的预期中考虑了侯世达定律。"
|
||||||
tip_premature_optimization: "过早的优化是万恶之源。 - 高德纳"
|
tip_premature_optimization: "过早的优化是万恶之源。 - 高德纳"
|
||||||
tip_brute_force: "拿不准时就用穷举法。 - Ken Thompson"
|
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."
|
# tip_superpower: "Coding is the closest thing we have to a superpower."
|
||||||
|
|
||||||
game_menu:
|
game_menu:
|
||||||
|
@ -315,8 +315,8 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
||||||
save_load_tab: "保存/打开"
|
save_load_tab: "保存/打开"
|
||||||
options_tab: "设置"
|
options_tab: "设置"
|
||||||
guide_tab: "使用向导"
|
guide_tab: "使用向导"
|
||||||
# guide_video_tutorial: "Video Tutorial"
|
guide_video_tutorial: "视频教程"
|
||||||
# guide_tips: "Tips"
|
guide_tips: "小技巧"
|
||||||
multiplayer_tab: "多人游戏"
|
multiplayer_tab: "多人游戏"
|
||||||
auth_tab: "注册"
|
auth_tab: "注册"
|
||||||
inventory_caption: "装备你的英雄"
|
inventory_caption: "装备你的英雄"
|
||||||
|
@ -330,7 +330,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
||||||
inventory:
|
inventory:
|
||||||
choose_inventory: "装备道具"
|
choose_inventory: "装备道具"
|
||||||
equipped_item: "已装备"
|
equipped_item: "已装备"
|
||||||
# required_purchase_title: "Required"
|
required_purchase_title: "需要"
|
||||||
available_item: "可用"
|
available_item: "可用"
|
||||||
restricted_title: "被限制"
|
restricted_title: "被限制"
|
||||||
should_equip: "(双击装备此道具)"
|
should_equip: "(双击装备此道具)"
|
||||||
|
@ -350,7 +350,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
||||||
prompt_title: "没有足够数量的宝石"
|
prompt_title: "没有足够数量的宝石"
|
||||||
prompt_body: "还需要更多吗?"
|
prompt_body: "还需要更多吗?"
|
||||||
prompt_button: "进入商店"
|
prompt_button: "进入商店"
|
||||||
# recovered: "Previous gems purchase recovered. Please refresh the page."
|
recovered: "之前购买的宝石已恢复。请刷新页面。"
|
||||||
|
|
||||||
subscribe:
|
subscribe:
|
||||||
subscribe_title: "订阅"
|
subscribe_title: "订阅"
|
||||||
|
@ -391,7 +391,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
||||||
regeneration: "恢复"
|
regeneration: "恢复"
|
||||||
range: "攻击范围" # As in "attack or visual range"
|
range: "攻击范围" # As in "attack or visual range"
|
||||||
blocks: "格挡" # As in "this shield blocks this much damage"
|
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: "技能"
|
skills: "技能"
|
||||||
available_for_purchase: "可以购买" # Shows up when you have unlocked, but not purchased, a hero in the hero store
|
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)
|
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:
|
classes:
|
||||||
archmage_title: "大法师"
|
archmage_title: "大法师"
|
||||||
archmage_title_description: "(代码编写人员)"
|
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: "工匠"
|
||||||
artisan_title_description: "(关卡建立人员)"
|
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: "冒险家"
|
||||||
adventurer_title_description: "(关卡测试人员)"
|
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."
|
# 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"
|
# achievement_query_goals: "Key achievement off of level goals"
|
||||||
level_completion: "关卡完成"
|
level_completion: "关卡完成"
|
||||||
pop_i18n: "填写 I18N"
|
pop_i18n: "填写 I18N"
|
||||||
# tasks: "Tasks"
|
tasks: "任务"
|
||||||
|
|
||||||
article:
|
article:
|
||||||
edit_btn_preview: "预览"
|
edit_btn_preview: "预览"
|
||||||
|
@ -829,7 +829,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
||||||
# service_apple: "Apple"
|
# service_apple: "Apple"
|
||||||
# service_web: "Web"
|
# service_web: "Web"
|
||||||
# paid_on: "Paid On"
|
# paid_on: "Paid On"
|
||||||
# service: "Service"
|
service: "服务"
|
||||||
price: "价格"
|
price: "价格"
|
||||||
gems: "宝石"
|
gems: "宝石"
|
||||||
# active: "Active"
|
# active: "Active"
|
||||||
|
@ -934,7 +934,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
||||||
practices_title: "尊重最佳实践"
|
practices_title: "尊重最佳实践"
|
||||||
practices_description: "这是我们对您的承诺,即玩家,尽管这在法律用语中略显不足。"
|
practices_description: "这是我们对您的承诺,即玩家,尽管这在法律用语中略显不足。"
|
||||||
privacy_title: "隐私"
|
privacy_title: "隐私"
|
||||||
# privacy_description: "We will not sell any of your personal information."
|
privacy_description: "我们不会泄露您的个人信息。"
|
||||||
security_title: "安全"
|
security_title: "安全"
|
||||||
security_description: "我们竭力保证您的个人信息安全性。作为一个开源项目,任何人都可以检讨并改善我们自由开放的网站的安全性。"
|
security_description: "我们竭力保证您的个人信息安全性。作为一个开源项目,任何人都可以检讨并改善我们自由开放的网站的安全性。"
|
||||||
email_title: "电子邮件"
|
email_title: "电子邮件"
|
||||||
|
@ -1026,7 +1026,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
||||||
next_photo: "添加一张职业的照片(可选)。"
|
next_photo: "添加一张职业的照片(可选)。"
|
||||||
next_active: "将自己标记为正在寻求工作机会以使自己的名字出现在搜索结果中。"
|
next_active: "将自己标记为正在寻求工作机会以使自己的名字出现在搜索结果中。"
|
||||||
example_blog: "你的博客"
|
example_blog: "你的博客"
|
||||||
# example_personal_site: "Personal Site"
|
example_personal_site: "个人主页"
|
||||||
links_header: "个人网站链接"
|
links_header: "个人网站链接"
|
||||||
links_blurb: "链接任何你希望展示的网站或介绍,例如你的Github,你的领英档案,或是你的博客。"
|
links_blurb: "链接任何你希望展示的网站或介绍,例如你的Github,你的领英档案,或是你的博客。"
|
||||||
links_name: "链接名称"
|
links_name: "链接名称"
|
||||||
|
@ -1088,7 +1088,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
||||||
education_description: "描述"
|
education_description: "描述"
|
||||||
education_description_help: "展示任何与你的教育经历相关的信息。(140个字符;选填)"
|
education_description_help: "展示任何与你的教育经历相关的信息。(140个字符;选填)"
|
||||||
our_notes: "我们的评注"
|
our_notes: "我们的评注"
|
||||||
# remarks: "Remarks"
|
remarks: "评价"
|
||||||
projects: "项目"
|
projects: "项目"
|
||||||
projects_header: "添加3个项目"
|
projects_header: "添加3个项目"
|
||||||
projects_header_2: "项目(前3个)"
|
projects_header_2: "项目(前3个)"
|
||||||
|
@ -1101,7 +1101,7 @@ module.exports = nativeDescription: "简体中文", englishDescription: "Chinese
|
||||||
project_picture_help: "上传一站230x115像素或更大的图片来展示这个项目。"
|
project_picture_help: "上传一站230x115像素或更大的图片来展示这个项目。"
|
||||||
project_link: "链接"
|
project_link: "链接"
|
||||||
project_link_help: "项目的链接。"
|
project_link_help: "项目的链接。"
|
||||||
# player_code: "Player Code"
|
player_code: "玩家代码"
|
||||||
|
|
||||||
employers:
|
employers:
|
||||||
# deprecation_warning_title: "Sorry, CodeCombat is not recruiting right now."
|
# 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_sub_title: "其他"
|
||||||
av_other_debug_base_url: "Base(用于调试 base.jade)"
|
av_other_debug_base_url: "Base(用于调试 base.jade)"
|
||||||
u_title: "用户列表"
|
u_title: "用户列表"
|
||||||
# ucp_title: "User Code Problems"
|
ucp_title: "用户代码的问题"
|
||||||
lg_title: "最新的游戏"
|
lg_title: "最新的游戏"
|
||||||
clas: "贡献者许可协议"
|
clas: "贡献者许可协议"
|
||||||
|
|
|
@ -84,7 +84,7 @@ module.exports = class User extends CocoModel
|
||||||
gemsEarned = @get('earned')?.gems ? 0
|
gemsEarned = @get('earned')?.gems ? 0
|
||||||
gemsPurchased = @get('purchased')?.gems ? 0
|
gemsPurchased = @get('purchased')?.gems ? 0
|
||||||
gemsSpent = @get('spent') ? 0
|
gemsSpent = @get('spent') ? 0
|
||||||
gemsEarned + gemsPurchased - gemsSpent
|
Math.floor gemsEarned + gemsPurchased - gemsSpent
|
||||||
|
|
||||||
heroes: ->
|
heroes: ->
|
||||||
heroes = (me.get('purchased')?.heroes ? []).concat([ThangType.heroes.captain, ThangType.heroes.knight])
|
heroes = (me.get('purchased')?.heroes ? []).concat([ThangType.heroes.captain, ThangType.heroes.knight])
|
||||||
|
|
|
@ -34,7 +34,7 @@ defaultTasks = [
|
||||||
'Write a loading tip, if needed.'
|
'Write a loading tip, if needed.'
|
||||||
'Click the Populate i18n button.'
|
'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.'
|
'Release to everyone via MailChimp.'
|
||||||
|
|
||||||
'Check completion/engagement/problem analytics.'
|
'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')
|
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}."},
|
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')
|
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}},
|
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'}
|
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
|
h4(data-i18n="contribute.how_to_join") How to Join
|
||||||
p
|
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
|
| Contact us
|
||||||
span ,
|
span ,
|
||||||
span(data-i18n="contribute.ambassador_join_desc")
|
span(data-i18n="contribute.ambassador_join_desc")
|
||||||
|
|
|
@ -41,13 +41,13 @@ block content
|
||||||
p
|
p
|
||||||
span(data-i18n="contribute.join_desc_1")
|
span(data-i18n="contribute.join_desc_1")
|
||||||
| Anyone can help out! Just check out our
|
| 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
|
| GitHub
|
||||||
span
|
span
|
||||||
span(data-i18n="contribute.join_desc_2")
|
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.
|
| 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?
|
| 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
|
| Email us
|
||||||
span(data-i18n="contribute.join_desc_3")
|
span(data-i18n="contribute.join_desc_3")
|
||||||
| , or find us in our
|
| , or find us in our
|
||||||
|
|
|
@ -36,7 +36,7 @@ block content
|
||||||
|
|
||||||
h4(data-i18n="contribute.how_to_join") How To Join
|
h4(data-i18n="contribute.how_to_join") How To Join
|
||||||
p
|
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
|
| Contact us
|
||||||
span ,
|
span ,
|
||||||
span(data-i18n="contribute.scribe_join_description")
|
span(data-i18n="contribute.scribe_join_description")
|
||||||
|
|
|
@ -65,7 +65,7 @@
|
||||||
|
|
||||||
h4 Recent Sessions
|
h4 Recent Sessions
|
||||||
if recentSessions
|
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
|
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')
|
table.table.table-bordered.table-condensed.table-hover(style='font-size:10pt')
|
||||||
thead
|
thead
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
.btn-group(data-toggle="buttons").status-buttons
|
.btn-group(data-toggle="buttons").status-buttons
|
||||||
label.btn.btn-default.pending
|
label.btn.btn-default.pending
|
||||||
input(type="radio", name="status", value="pending")
|
input(type="radio", name="status", value="pending", data-i18n="general.pending")
|
||||||
| Pending
|
| Pending
|
||||||
label.btn.btn-default.accepted
|
label.btn.btn-default.accepted
|
||||||
input(type="radio", name="status", value="accepted")
|
input(type="radio", name="status", value="accepted", data-i18n="general.accepted")
|
||||||
| Accepted
|
| Accepted
|
||||||
label.btn.btn-default.rejected
|
label.btn.btn-default.rejected
|
||||||
input(type="radio", name="status", value="rejected")
|
input(type="radio", name="status", value="rejected", data-i18n="general.rejected")
|
||||||
| Rejected
|
| Rejected
|
||||||
label.btn.btn-default.withdrawn
|
label.btn.btn-default.withdrawn
|
||||||
input(type="radio", name="status", value="withdrawn")
|
input(type="radio", name="status", value="withdrawn", data-i18n="general.withdrawn")
|
||||||
| Withdrawn
|
| Withdrawn
|
||||||
|
|
||||||
if patches.loading
|
if patches.loading
|
||||||
|
|
|
@ -243,9 +243,7 @@ module.exports = class CampaignEditorView extends RootView
|
||||||
getCampaignCompletions: =>
|
getCampaignCompletions: =>
|
||||||
# Fetch last 7 days of campaign drop-off rates
|
# Fetch last 7 days of campaign drop-off rates
|
||||||
|
|
||||||
startDay = new Date()
|
startDay = utils.getUTCDay -6
|
||||||
startDay.setDate(startDay.getUTCDate() - 6)
|
|
||||||
startDay = startDay.getUTCFullYear() + '-' + (startDay.getUTCMonth() + 1) + '-' + startDay.getUTCDate()
|
|
||||||
|
|
||||||
success = (data) =>
|
success = (data) =>
|
||||||
return if @destroyed
|
return if @destroyed
|
||||||
|
|
|
@ -3,6 +3,7 @@ Level = require 'models/Level'
|
||||||
LevelSession = require 'models/LevelSession'
|
LevelSession = require 'models/LevelSession'
|
||||||
ModelModal = require 'views/modal/ModelModal'
|
ModelModal = require 'views/modal/ModelModal'
|
||||||
User = require 'models/User'
|
User = require 'models/User'
|
||||||
|
utils = require 'core/utils'
|
||||||
|
|
||||||
module.exports = class CampaignLevelView extends CocoView
|
module.exports = class CampaignLevelView extends CocoView
|
||||||
id: 'campaign-level-view'
|
id: 'campaign-level-view'
|
||||||
|
@ -47,9 +48,7 @@ module.exports = class CampaignLevelView extends CocoView
|
||||||
|
|
||||||
getCommonLevelProblems: ->
|
getCommonLevelProblems: ->
|
||||||
# Fetch last 30 days of common level problems
|
# Fetch last 30 days of common level problems
|
||||||
startDay = new Date()
|
startDay = utils.getUTCDay -29
|
||||||
startDay.setDate(startDay.getUTCDate() - 29)
|
|
||||||
startDay = startDay.getUTCFullYear() + '-' + (startDay.getUTCMonth() + 1) + '-' + startDay.getUTCDate()
|
|
||||||
|
|
||||||
success = (data) =>
|
success = (data) =>
|
||||||
return if @destroyed
|
return if @destroyed
|
||||||
|
@ -77,9 +76,7 @@ module.exports = class CampaignLevelView extends CocoView
|
||||||
@levelCompletions = _.map data, mapFn, @
|
@levelCompletions = _.map data, mapFn, @
|
||||||
@render()
|
@render()
|
||||||
|
|
||||||
startDay = new Date()
|
startDay = utils.getUTCDay -6
|
||||||
startDay.setDate(startDay.getUTCDate() - 6)
|
|
||||||
startDay = startDay.getUTCFullYear() + '-' + (startDay.getUTCMonth() + 1) + '-' + startDay.getUTCDate()
|
|
||||||
|
|
||||||
# TODO: Why do we need this url dash?
|
# TODO: Why do we need this url dash?
|
||||||
request = @supermodel.addRequestResource 'level_completions', {
|
request = @supermodel.addRequestResource 'level_completions', {
|
||||||
|
@ -97,9 +94,7 @@ module.exports = class CampaignLevelView extends CocoView
|
||||||
@levelPlaytimes = data.sort (a, b) -> if a.created < b.created then 1 else -1
|
@levelPlaytimes = data.sort (a, b) -> if a.created < b.created then 1 else -1
|
||||||
@render()
|
@render()
|
||||||
|
|
||||||
startDay = new Date()
|
startDay = utils.getUTCDay -6
|
||||||
startDay.setDate(startDay.getUTCDate() - 6)
|
|
||||||
startDay = startDay.getUTCFullYear() + '-' + (startDay.getUTCMonth() + 1) + '-' + startDay.getUTCDate()
|
|
||||||
|
|
||||||
# TODO: Why do we need this url dash?
|
# TODO: Why do we need this url dash?
|
||||||
request = @supermodel.addRequestResource 'playtime_averages', {
|
request = @supermodel.addRequestResource 'playtime_averages', {
|
||||||
|
@ -111,7 +106,7 @@ module.exports = class CampaignLevelView extends CocoView
|
||||||
request.load()
|
request.load()
|
||||||
|
|
||||||
getRecentSessions: ->
|
getRecentSessions: ->
|
||||||
limit = 10
|
limit = 100
|
||||||
|
|
||||||
success = (data) =>
|
success = (data) =>
|
||||||
return if @destroyed
|
return if @destroyed
|
||||||
|
|
|
@ -110,6 +110,8 @@ module.exports = class HeroVictoryModal extends ModalView
|
||||||
# rewards = achievement.get('rewards') or {}
|
# rewards = achievement.get('rewards') or {}
|
||||||
# rewards.gems *= (index + 1)
|
# rewards.gems *= (index + 1)
|
||||||
|
|
||||||
|
# TODO: use earned achievement worths or somehow pull in recalculated exp/gems
|
||||||
|
|
||||||
c.thangTypes = @thangTypes
|
c.thangTypes = @thangTypes
|
||||||
c.me = me
|
c.me = me
|
||||||
c.readyToRank = @level.get('type', true) is 'hero-ladder' and @session.readyToRank()
|
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();
|
var todayMinus6 = new Date();
|
||||||
todayMinus6.setDate(todayMinus6.getUTCDate() - 6);
|
todayMinus6.setDate(todayMinus6.getUTCDate() - 6);
|
||||||
var startDate = todayMinus6.toISOString().substr(0, 10) + "T00:00:00.000Z";
|
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)
|
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({
|
var cursor = db['analytics.log.events'].find({
|
||||||
$and: [
|
$and: [
|
||||||
{"created": { $gte: ISODate(startDate)}},
|
{"created": { $gte: ISODate(startDate)}},
|
||||||
|
// {"created": { $lt: ISODate(endDate)}},
|
||||||
{$or: [ {"event" : 'Started Level'}, {"event" : 'Saw Victory'}]}
|
{$or: [ {"event" : 'Started Level'}, {"event" : 'Saw Victory'}]}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
@ -115,6 +118,7 @@ var campaigns = {
|
||||||
// Bucketize events by user
|
// Bucketize events by user
|
||||||
print("Getting event data...");
|
print("Getting event data...");
|
||||||
var userProgression = {};
|
var userProgression = {};
|
||||||
|
var userLevelEventMap = {}; // Only want unique users per-level/event
|
||||||
while (cursor.hasNext()) {
|
while (cursor.hasNext()) {
|
||||||
var doc = cursor.next();
|
var doc = cursor.next();
|
||||||
var created = doc.created;
|
var created = doc.created;
|
||||||
|
@ -124,12 +128,17 @@ while (cursor.hasNext()) {
|
||||||
if (level) {
|
if (level) {
|
||||||
if (level.length > longestLevelName) longestLevelName = level.length;
|
if (level.length > longestLevelName) longestLevelName = level.length;
|
||||||
var user = doc.user.valueOf();
|
var user = doc.user.valueOf();
|
||||||
if (!userProgression[user]) userProgression[user] = [];
|
if (!userLevelEventMap[user]) userLevelEventMap[user] = {};
|
||||||
userProgression[user].push({
|
if (!userLevelEventMap[user][level]) userLevelEventMap[user][level] = {};
|
||||||
created: created,
|
if (!userLevelEventMap[user][level][event]) {
|
||||||
event: event,
|
userLevelEventMap[user][level][event] = true;
|
||||||
level: level
|
if (!userProgression[user]) userProgression[user] = [];
|
||||||
});
|
userProgression[user].push({
|
||||||
|
created: created,
|
||||||
|
event: event,
|
||||||
|
level: level
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
longestLevelName += 2;
|
longestLevelName += 2;
|
||||||
|
@ -224,7 +233,7 @@ for (campaign in campaigns) {
|
||||||
print("\n" + campaign);
|
print("\n" + campaign);
|
||||||
var level = "level";
|
var level = "level";
|
||||||
var levelSpacer = new Array(longestLevelName - level.length).join(' ');
|
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++) {
|
for (var i = 0; i < campaignRates[campaign].levels.length; i++) {
|
||||||
var level = campaignRates[campaign].levels[i].level;
|
var level = campaignRates[campaign].levels[i].level;
|
||||||
var started = campaignRates[campaign].levels[i].started;
|
var started = campaignRates[campaign].levels[i].started;
|
||||||
|
@ -232,13 +241,13 @@ for (campaign in campaigns) {
|
||||||
var finished = campaignRates[campaign].levels[i].finished;
|
var finished = campaignRates[campaign].levels[i].finished;
|
||||||
var finishDropped = campaignRates[campaign].levels[i].finishDropped;
|
var finishDropped = campaignRates[campaign].levels[i].finishDropped;
|
||||||
var levelSpacer = new Array(longestLevelName - level.length).join(' ');
|
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 level = 'Overall';
|
||||||
var started = campaignRates[campaign].overall.started;
|
// var started = campaignRates[campaign].overall.started;
|
||||||
var startDropped = campaignRates[campaign].overall.startDropped;
|
// var startDropped = campaignRates[campaign].overall.startDropped;
|
||||||
var finished = campaignRates[campaign].overall.finished;
|
// var finished = campaignRates[campaign].overall.finished;
|
||||||
var finishDropped = campaignRates[campaign].overall.finishDropped;
|
// var finishDropped = campaignRates[campaign].overall.finishDropped;
|
||||||
var levelSpacer = new Array(longestLevelName - level.length).join(' ');
|
// 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) + "%");
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ EarnedAchievementSchema.pre 'save', (next) ->
|
||||||
EarnedAchievementSchema.index({user: 1, achievement: 1}, {unique: true, name: 'earned achievement index'})
|
EarnedAchievementSchema.index({user: 1, achievement: 1}, {unique: true, name: 'earned achievement index'})
|
||||||
EarnedAchievementSchema.index({user: 1, changed: -1}, {name: 'latest '})
|
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'
|
User = require '../users/User'
|
||||||
userObjectID = doc.get(achievement.get('userField'))
|
userObjectID = doc.get(achievement.get('userField'))
|
||||||
userID = if _.isObject userObjectID then userObjectID.toHexString() else userObjectID # Standardize! Use strings, not ObjectId's
|
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'
|
achievementName: achievement.get 'name'
|
||||||
earnedRewards: achievement.get 'rewards'
|
earnedRewards: achievement.get 'rewards'
|
||||||
|
|
||||||
worth = achievement.get('worth') ? 10
|
pointWorth = achievement.get('worth') ? 10
|
||||||
|
gemWorth = achievement.get('rewards')?.gems ? 0
|
||||||
earnedPoints = 0
|
earnedPoints = 0
|
||||||
|
earnedGems = 0
|
||||||
|
|
||||||
wrapUp = (earnedAchievementDoc) ->
|
wrapUp = (earnedAchievementDoc) ->
|
||||||
# Update user's experience points
|
# Update user's experience points
|
||||||
update = {$inc: {points: earnedPoints}}
|
update = {$inc: {points: earnedPoints, 'earned.gems': earnedGems}}
|
||||||
for rewardType, rewards of achievement.get('rewards') ? {}
|
for rewardType, rewards of achievement.get('rewards') ? {}
|
||||||
if rewardType is 'gems'
|
continue if rewardType is 'gems'
|
||||||
update.$inc['earned.gems'] = rewards if rewards
|
if rewards.length
|
||||||
else if rewards.length
|
|
||||||
update.$addToSet ?= {}
|
update.$addToSet ?= {}
|
||||||
update.$addToSet["earned.#{rewardType}"] = $each: rewards
|
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?
|
log.error err if err?
|
||||||
done?(earnedAchievementDoc)
|
done?(earnedAchievementDoc)
|
||||||
|
|
||||||
|
@ -46,29 +48,40 @@ EarnedAchievementSchema.statics.createForAchievement = (achievement, doc, origin
|
||||||
if isRepeatable
|
if isRepeatable
|
||||||
#log.debug 'Upserting repeatable achievement called \'' + (achievement.get 'name') + '\' for ' + userID
|
#log.debug 'Upserting repeatable achievement called \'' + (achievement.get 'name') + '\' for ' + userID
|
||||||
proportionalTo = achievement.get 'proportionalTo'
|
proportionalTo = achievement.get 'proportionalTo'
|
||||||
originalAmount = if originalDocObj then util.getByPath(originalDocObj, proportionalTo) or 0 else 0
|
|
||||||
docObj = doc.toObject()
|
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
|
if originalAmount isnt newAmount
|
||||||
expFunction = achievement.getExpFunction()
|
expFunction = achievement.getExpFunction()
|
||||||
earned.notified = false
|
earned.notified = false
|
||||||
earned.achievedAmount = newAmount
|
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
|
earned.previouslyAchievedAmount = originalAmount
|
||||||
EarnedAchievement.update {achievement: earned.achievement, user: earned.user}, earned, {upsert: true}, (err) ->
|
EarnedAchievement.update {achievement: earned.achievement, user: earned.user}, earned, {upsert: true}, (err) ->
|
||||||
return log.debug err if err?
|
return log.debug err if err?
|
||||||
|
|
||||||
earnedPoints = earned.earnedPoints
|
|
||||||
#log.debug earnedPoints
|
#log.debug earnedPoints
|
||||||
wrapUp()
|
wrapUp(new EarnedAchievement(earned))
|
||||||
|
else
|
||||||
|
done?()
|
||||||
|
|
||||||
else # not alreadyAchieved
|
else # not alreadyAchieved
|
||||||
#log.debug 'Creating a new earned achievement called \'' + (achievement.get 'name') + '\' for ' + userID
|
#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) ->
|
(new EarnedAchievement(earned)).save (err, doc) ->
|
||||||
return log.error err if err?
|
return log.error err if err?
|
||||||
earnedPoints = worth
|
earnedPoints = pointWorth
|
||||||
|
earnedGems = gemWorth
|
||||||
wrapUp(doc)
|
wrapUp(doc)
|
||||||
|
|
||||||
User.saveActiveUser userID, "achievement"
|
User.saveActiveUser userID, "achievement"
|
||||||
|
|
|
@ -6,6 +6,7 @@ EarnedAchievement = require './EarnedAchievement'
|
||||||
User = require '../users/User'
|
User = require '../users/User'
|
||||||
Handler = require '../commons/Handler'
|
Handler = require '../commons/Handler'
|
||||||
LocalMongo = require '../../app/lib/LocalMongo'
|
LocalMongo = require '../../app/lib/LocalMongo'
|
||||||
|
util = require '../../app/core/utils'
|
||||||
|
|
||||||
class EarnedAchievementHandler extends Handler
|
class EarnedAchievementHandler extends Handler
|
||||||
modelClass: EarnedAchievement
|
modelClass: EarnedAchievement
|
||||||
|
@ -65,6 +66,10 @@ class EarnedAchievementHandler extends Handler
|
||||||
return @sendNotFoundError(res, 'Could not find achievement.')
|
return @sendNotFoundError(res, 'Could not find achievement.')
|
||||||
else if not trigger
|
else if not trigger
|
||||||
return @sendNotFoundError(res, 'Could not find 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
|
else if earned
|
||||||
achievementEarned = achievement.get('rewards')
|
achievementEarned = achievement.get('rewards')
|
||||||
actuallyEarned = earned.get('earnedRewards')
|
actuallyEarned = earned.get('earnedRewards')
|
||||||
|
@ -82,10 +87,8 @@ class EarnedAchievementHandler extends Handler
|
||||||
return @sendDatabaseError(res, err) if err
|
return @sendDatabaseError(res, err) if err
|
||||||
return @sendSuccess(res, earned.toObject())
|
return @sendSuccess(res, earned.toObject())
|
||||||
)
|
)
|
||||||
else if achievement.get('proportionalTo')
|
|
||||||
return @sendBadInputError(res, 'Cannot currently do this to repeatable docs...')
|
|
||||||
else
|
else
|
||||||
EarnedAchievement.createForAchievement(achievement, trigger, null, (earnedAchievementDoc) =>
|
EarnedAchievement.createForAchievement(achievement, trigger, null, null, (earnedAchievementDoc) =>
|
||||||
@sendCreated(res, earnedAchievementDoc.toObject())
|
@sendCreated(res, earnedAchievementDoc.toObject())
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -221,13 +224,15 @@ class EarnedAchievementHandler extends Handler
|
||||||
notified: achievement._id in alreadyEarnedIDs
|
notified: achievement._id in alreadyEarnedIDs
|
||||||
|
|
||||||
if isRepeatable
|
if isRepeatable
|
||||||
earned.achievedAmount = something.get(achievement.get 'proportionalTo')
|
earned.achievedAmount = util.getByPath(something.toObject(), achievement.get 'proportionalTo') or 0
|
||||||
earned.previouslyAchievedAmount = 0
|
earned.previouslyAchievedAmount = 0
|
||||||
|
|
||||||
expFunction = achievement.getExpFunction()
|
expFunction = achievement.getExpFunction()
|
||||||
newPoints = expFunction(earned.achievedAmount) * achievement.get('worth') ? 10
|
newPoints = expFunction(earned.achievedAmount) * achievement.get('worth') ? 10
|
||||||
|
newGems = expFunction(earned.achievedAmount) * (achievement.get('rewards')?.gems ? 0)
|
||||||
else
|
else
|
||||||
newPoints = achievement.get('worth') ? 10
|
newPoints = achievement.get('worth') ? 10
|
||||||
|
newGems = achievement.get('rewards')?.gems ? 0
|
||||||
|
|
||||||
earned.earnedPoints = newPoints
|
earned.earnedPoints = newPoints
|
||||||
newTotalPoints += newPoints
|
newTotalPoints += newPoints
|
||||||
|
@ -235,7 +240,10 @@ class EarnedAchievementHandler extends Handler
|
||||||
earned.earnedRewards = achievement.get('rewards')
|
earned.earnedRewards = achievement.get('rewards')
|
||||||
for rewardType in ['heroes', 'items', 'levels']
|
for rewardType in ['heroes', 'items', 'levels']
|
||||||
newTotalRewards[rewardType] = newTotalRewards[rewardType].concat(achievement.get('rewards')?[rewardType] ? [])
|
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) ->
|
EarnedAchievement.update {achievement:earned.achievement, user:earned.user}, earned, {upsert: true}, (err) ->
|
||||||
doneWithAchievement err
|
doneWithAchievement err
|
||||||
|
|
|
@ -33,6 +33,7 @@ class AnalyticsLogEventHandler extends Handler
|
||||||
# endDay - Exclusive, optional, e.g. '2014-12-16'
|
# endDay - Exclusive, optional, e.g. '2014-12-16'
|
||||||
|
|
||||||
# TODO: An uncached call takes about 15s locally
|
# TODO: An uncached call takes about 15s locally
|
||||||
|
# TODO: Use unique users
|
||||||
|
|
||||||
levelSlug = req.query.slug or req.body.slug
|
levelSlug = req.query.slug or req.body.slug
|
||||||
startDay = req.query.startDay or req.body.startDay
|
startDay = req.query.startDay or req.body.startDay
|
||||||
|
@ -105,11 +106,12 @@ class AnalyticsLogEventHandler extends Handler
|
||||||
# startDay - Inclusive, optional, e.g. '2014-12-14'
|
# startDay - Inclusive, optional, e.g. '2014-12-14'
|
||||||
# endDay - Exclusive, optional, e.g. '2014-12-16'
|
# 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: An uncached call can take over 30s locally
|
||||||
# TODO: Returns all the campaigns
|
# TODO: Returns all the campaigns
|
||||||
# TODO: Calculate overall campaign stats
|
# TODO: Calculate overall campaign stats
|
||||||
# TODO: Assumes db campaign levels are in progression order. Should build this based on actual progression.
|
# 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
|
campaignSlug = req.query.slug or req.body.slug
|
||||||
startDay = req.query.startDay or req.body.startDay
|
startDay = req.query.startDay or req.body.startDay
|
||||||
|
@ -128,95 +130,111 @@ class AnalyticsLogEventHandler extends Handler
|
||||||
cacheKey += 'e' + endDay if endDay?
|
cacheKey += 'e' + endDay if endDay?
|
||||||
return @sendSuccess res, campaignDropOffs if campaignDropOffs = @campaignDropOffsCache[cacheKey]
|
return @sendSuccess res, campaignDropOffs if campaignDropOffs = @campaignDropOffsCache[cacheKey]
|
||||||
|
|
||||||
getCompletions = (campaigns) =>
|
getCompletions = (campaigns, userProgression) =>
|
||||||
# Calculate campaign drop off rates
|
# Calculate campaign drop off rates
|
||||||
# Input:
|
# Input:
|
||||||
# campaigns - per-campaign dictionary of ordered level slugs
|
# 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: [{$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: {$gte: new Date(startDay + "T00:00:00.000Z")} if startDay?
|
||||||
queryParams["$and"].push created: {$lt: new Date(endDay + "T00:00:00.000Z")} if endDay?
|
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) =>
|
# Query stream is better for large results
|
||||||
if err? then return @sendDatabaseError res, err
|
# http://mongoosejs.com/docs/api.html#query_Query-stream
|
||||||
|
stream = AnalyticsLogEvent.find(queryParams).select('created event properties user').stream()
|
||||||
# Bucketize events by user
|
stream.on 'data', (item) =>
|
||||||
userProgression = {}
|
created = item.get('created')
|
||||||
for item in data
|
event = item.get('event')
|
||||||
created = item.get('created')
|
if event is 'Saw Victory'
|
||||||
event = item.get('event')
|
level = item.get('properties.level').toLowerCase().replace new RegExp(' ', 'g'), '-'
|
||||||
if event is 'Saw Victory'
|
else
|
||||||
level = item.get('properties.level').toLowerCase().replace new RegExp(' ', 'g'), '-'
|
level = item.get('properties.levelID')
|
||||||
else
|
return unless level?
|
||||||
level = item.get('properties.levelID')
|
user = item.get('user')
|
||||||
continue unless level?
|
userProgression[user] ?= []
|
||||||
user = item.get('user')
|
userProgression[user].push
|
||||||
userProgression[user] ?= []
|
created: created
|
||||||
userProgression[user].push
|
event: event
|
||||||
created: created
|
level: level
|
||||||
event: event
|
.on 'error', (err) =>
|
||||||
level: level
|
return @sendDatabaseError res, err
|
||||||
|
.on 'close', () =>
|
||||||
# Order user progression by created
|
getCompletions campaigns, userProgression
|
||||||
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
|
|
||||||
|
|
||||||
getLevelData = (campaigns, campaignLevelIDs) =>
|
getLevelData = (campaigns, campaignLevelIDs) =>
|
||||||
# Get level data and replace levelIDs with level slugs in campaigns
|
# Get level data and replace levelIDs with level slugs in campaigns
|
||||||
|
@ -240,7 +258,7 @@ class AnalyticsLogEventHandler extends Handler
|
||||||
mapFn = (item) -> levelSlugMap[item]
|
mapFn = (item) -> levelSlugMap[item]
|
||||||
campaigns[campaign] = _.map campaigns[campaign], mapFn, @
|
campaigns[campaign] = _.map campaigns[campaign], mapFn, @
|
||||||
|
|
||||||
getCompletions campaigns
|
getUserEventData campaigns
|
||||||
|
|
||||||
getCampaignData = () =>
|
getCampaignData = () =>
|
||||||
# Get campaign data
|
# Get campaign data
|
||||||
|
|
|
@ -39,7 +39,7 @@ CampaignHandler = class CampaignHandler extends Handler
|
||||||
|
|
||||||
getRelatedLevels: (req, res, campaign, projection) ->
|
getRelatedLevels: (req, res, campaign, projection) ->
|
||||||
extraProjectionProps = []
|
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.
|
# 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 'permissions' unless projection.permissions
|
||||||
extraProjectionProps.push 'version' unless projection.version
|
extraProjectionProps.push 'version' unless projection.version
|
||||||
|
|
|
@ -14,7 +14,8 @@ AchievablePlugin = (schema, options) ->
|
||||||
|
|
||||||
# Keep track the document before it's saved
|
# Keep track the document before it's saved
|
||||||
schema.post 'init', (doc) ->
|
schema.post 'init', (doc) ->
|
||||||
doc.unchangedCopy = doc.toObject()
|
unless doc.unchangedCopy
|
||||||
|
doc.unchangedCopy = doc.toObject()
|
||||||
|
|
||||||
# Check if an achievement has been earned
|
# Check if an achievement has been earned
|
||||||
schema.post 'save', (doc) ->
|
schema.post 'save', (doc) ->
|
||||||
|
|
|
@ -575,8 +575,8 @@ sendLadderUpdateEmail = (session, now, daysAgo) ->
|
||||||
unless user.get('email') and allowNotes and not session.unsubscribed
|
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}"
|
#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
|
return
|
||||||
unless session.levelName
|
unless session.levelName and session.team
|
||||||
#log.info "Not sending email to #{user.get('email')} #{user.get('name')} because the session had no levelName in it."
|
#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
|
return
|
||||||
name = if user.get('firstName') and user.get('lastName') then "#{user.get('firstName')}" else user.get('name')
|
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'
|
name = 'Wizard' if not name or name is 'Anoner'
|
||||||
|
|
|
@ -21,6 +21,8 @@ repeatable =
|
||||||
userField: '_id'
|
userField: '_id'
|
||||||
proportionalTo: 'simulatedBy'
|
proportionalTo: 'simulatedBy'
|
||||||
recalculable: true
|
recalculable: true
|
||||||
|
rewards:
|
||||||
|
gems: 1
|
||||||
|
|
||||||
diminishing =
|
diminishing =
|
||||||
name: 'Simulated2'
|
name: 'Simulated2'
|
||||||
|
@ -169,19 +171,23 @@ describe 'Achieving Achievements', ->
|
||||||
|
|
||||||
it 'verify that a repeatable achievement has been earned', (done) ->
|
it 'verify that a repeatable achievement has been earned', (done) ->
|
||||||
unittest.getNormalJoe (joe) ->
|
unittest.getNormalJoe (joe) ->
|
||||||
EarnedAchievement.find {achievementName: repeatable.name}, (err, docs) ->
|
|
||||||
expect(err).toBeNull()
|
|
||||||
expect(docs.length).toBe(1)
|
|
||||||
achievement = docs[0]
|
|
||||||
|
|
||||||
if achievement
|
User.findById(joe.get('_id')).exec (err, joe2) ->
|
||||||
expect(achievement.get 'achievement').toBe repeatable._id
|
expect(joe2.get('earned').gems).toBe(2)
|
||||||
expect(achievement.get 'user').toBe joe._id.toHexString()
|
|
||||||
expect(achievement.get 'notified').toBeFalsy()
|
EarnedAchievement.find {achievementName: repeatable.name}, (err, docs) ->
|
||||||
expect(achievement.get 'earnedPoints').toBe 2 * repeatable.worth
|
expect(err).toBeNull()
|
||||||
expect(achievement.get 'achievedAmount').toBe 2
|
expect(docs.length).toBe(1)
|
||||||
expect(achievement.get 'previouslyAchievedAmount').toBeFalsy()
|
achievement = docs[0]
|
||||||
done()
|
|
||||||
|
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) ->
|
it 'verify that the repeatable achievement with complex exp has been earned', (done) ->
|
||||||
unittest.getNormalJoe (joe) ->
|
unittest.getNormalJoe (joe) ->
|
||||||
|
@ -196,6 +202,17 @@ describe 'Achieving Achievements', ->
|
||||||
|
|
||||||
done()
|
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', ->
|
describe 'Recalculate Achievements', ->
|
||||||
EarnedAchievementHandler = require '../../../server/achievements/earned_achievement_handler'
|
EarnedAchievementHandler = require '../../../server/achievements/earned_achievement_handler'
|
||||||
|
|
||||||
|
@ -241,7 +258,8 @@ describe 'Recalculate Achievements', ->
|
||||||
unittest.getNormalJoe (joe) ->
|
unittest.getNormalJoe (joe) ->
|
||||||
User.findById joe.get('id'), (err, guy) ->
|
User.findById joe.get('id'), (err, guy) ->
|
||||||
expect(err).toBeNull()
|
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()
|
done()
|
||||||
|
|
||||||
it 'cleaning up test: deleting all Achievements and related', (done) ->
|
it 'cleaning up test: deleting all Achievements and related', (done) ->
|
||||||
|
|
Loading…
Reference in a new issue