Merge branch 'master' into production

This commit is contained in:
Nick Winter 2015-06-17 18:17:22 -07:00
commit bc8e0945a1
25 changed files with 565 additions and 222 deletions

View file

@ -69,3 +69,10 @@ Whether you're novice or pro, the CodeCombat team is ready to help you implement
![Alex Crooks](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Alex%20Crooks/alex_100.png)
![Danny Whittaker](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Danny%20Whittaker/danny_100.png)
![Kevin Holland](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Kevin%20Holland/kevin_100.png)
![Joachim Brehmer](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Joachim%20Brehmer/joachim_100.png)
![Jose Antonini](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Jose%20Antonini/jose_antonini_100.png)
![Oleg Ulyanicky](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Oleg%20Ulyanickiy/oleg_100.png)
![Pavel Konstantynov](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Pavel%20Konstantinov/pavel_100.png)
![Popey Gilbert](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Popey%20Gilbert/popey_100.png)
![Rob Blanckaert](https://dl.dropboxusercontent.com/u/138899/GitHub%20Wikis/avatars/Rob%20Blanckaert/rob_blanckaert_100.png)

Binary file not shown.

After

(image error) Size: 12 KiB

Binary file not shown.

After

(image error) Size: 9.3 KiB

Binary file not shown.

After

(image error) Size: 10 KiB

View file

@ -1,6 +1,6 @@
SpriteBuilder = require 'lib/sprites/SpriteBuilder'
floors = ['Dungeon Floor', 'Indoor Floor', 'Grass', 'Grass01', 'Grass02', 'Grass03', 'Grass04', 'Grass05', 'Goal Trigger', 'Obstacle', 'Sand 01', 'Sand 02', 'Sand 03', 'Sand 04', 'Sand 05', 'Sand 06', 'Talus 1', 'Talus 2', 'Talus 3', 'Talus 4', 'Talus 5', 'Talus 6', 'Firn 1', 'Firn 2', 'Firn 3', 'Firn 4', 'Firn 5', 'Firn 6']
floors = ['Dungeon Floor', 'Indoor Floor', 'Grass', 'Grass01', 'Grass02', 'Grass03', 'Grass04', 'Grass05', 'Goal Trigger', 'Obstacle', 'Sand 01', 'Sand 02', 'Sand 03', 'Sand 04', 'Sand 05', 'Sand 06', 'Talus 1', 'Talus 2', 'Talus 3', 'Talus 4', 'Talus 5', 'Talus 6', 'Firn 1', 'Firn 2', 'Firn 3', 'Firn 4', 'Firn 5', 'Firn 6', 'Ice Rink 1', 'Ice Rink 2', 'Ice Rink 3']
module.exports = class SingularSprite extends createjs.Sprite
childMovieClips: null

View file

@ -260,62 +260,62 @@ module.exports = nativeDescription: "dansk", englishDescription: "Danish", trans
victory_review: "Fortæl os mere!" # Only in old-style levels.
victory_hour_of_code_done: "Er du færdig?"
victory_hour_of_code_done_yes: "Ja, jeg er færdig med min Kodetime!"
# victory_experience_gained: "XP Gained"
# victory_gems_gained: "Gems Gained"
# victory_new_item: "New Item"
# victory_viking_code_school: "Holy smokes, that was a hard level you just beat! If you aren't already a software developer, you should be. You just got fast-tracked for acceptance with Viking Code School, where you can take your skills to the next level and become a professional web developer in 14 weeks."
# victory_become_a_viking: "Become a Viking"
victory_experience_gained: "XP tjent"
victory_gems_gained: "Diamanter tjent"
victory_new_item: "Nyt udstyr"
victory_viking_code_school: "For dælen det var en svær bane du lige slog! Hvis ikke du allerede er softwareudvikler, så burde du blive det. Du er lige kommet foran i køen til at blive accepteret hos Viking Code School, du kan tage dine evner til det næste niveau og blive en professionel webudvikler på 14 uger."
victory_become_a_viking: "Bliv en Viking"
guide_title: "Instruktioner"
# tome_minion_spells: "Your Minions' Spells" # Only in old-style levels.
# tome_read_only_spells: "Read-Only Spells" # Only in old-style levels.
tome_minion_spells: "Dine Minions' besværgelser" # Only in old-style levels.
tome_read_only_spells: "Læsebesværgelser" # Only in old-style levels.
tome_other_units: "Andre enheder" # Only in old-style levels.
# tome_cast_button_run: "Run"
# tome_cast_button_running: "Running"
# tome_cast_button_ran: "Ran"
# tome_submit_button: "Submit"
# tome_reload_method: "Reload original code for this method" # Title text for individual method reload button.
# tome_select_method: "Select a Method"
# tome_see_all_methods: "See all methods you can edit" # Title text for method list selector (shown when there are multiple programmable methods).
tome_cast_button_run: "Kør"
tome_cast_button_running: "Kører"
tome_cast_button_ran: "Kørt"
tome_submit_button: "Indsend"
tome_reload_method: "Genindlæs den originale kode til denne metode" # Title text for individual method reload button.
tome_select_method: "Vælg en metode"
tome_see_all_methods: "Se alle metoder du kan redigere" # Title text for method list selector (shown when there are multiple programmable methods).
tome_select_a_thang: "Vælg nogen til at "
tome_available_spells: "Tilgængelige trylleformularer"
# tome_your_skills: "Your Skills"
# tome_help: "Help"
# tome_current_method: "Current Method"
# hud_continue_short: "Continue"
# code_saved: "Code Saved"
tome_your_skills: "Dine evner"
tome_help: "Hjælp"
tome_current_method: "Nuværende metode"
hud_continue_short: "Fortsæt"
code_saved: "Kode gemt"
skip_tutorial: "Spring over (esc)"
# keyboard_shortcuts: "Key Shortcuts"
# loading_ready: "Ready!"
# loading_start: "Start Level"
# problem_alert_title: "Fix Your Code"
# problem_alert_help: "Help"
# time_current: "Now:"
# time_total: "Max:"
# time_goto: "Go to:"
# non_user_code_problem_title: "Unable to Load Level"
# infinite_loop_title: "Infinite Loop Detected"
# infinite_loop_description: "The initial code to build the world never finished running. It's probably either really slow or has an infinite loop. Or there might be a bug. You can either try running this code again or reset the code to the default state. If that doesn't fix it, please let us know."
# check_dev_console: "You can also open the developer console to see what might be going wrong."
# check_dev_console_link: "(instructions)"
# infinite_loop_try_again: "Try Again"
# infinite_loop_reset_level: "Reset Level"
# infinite_loop_comment_out: "Comment Out My Code"
# tip_toggle_play: "Toggle play/paused with Ctrl+P."
# tip_scrub_shortcut: "Use Ctrl+[ and Ctrl+] to rewind and fast-forward."
# tip_guide_exists: "Click the guide, inside game menu (at the top of the page), for useful info."
# tip_open_source: "CodeCombat is 100% open source!"
# tip_beta_launch: "CodeCombat launched its beta in October, 2013."
# tip_think_solution: "Think of the solution, not the problem."
# tip_theory_practice: "In theory, there is no difference between theory and practice. But in practice, there is. - Yogi Berra"
# tip_error_free: "There are two ways to write error-free programs; only the third one works. - Alan Perlis"
# tip_debugging_program: "If debugging is the process of removing bugs, then programming must be the process of putting them in. - Edsger W. Dijkstra"
# tip_forums: "Head over to the forums and tell us what you think!"
# tip_baby_coders: "In the future, even babies will be Archmages."
# tip_morale_improves: "Loading will continue until morale improves."
# tip_all_species: "We believe in equal opportunities to learn programming for all species."
keyboard_shortcuts: "Tastaturgenveje"
loading_ready: "Klar!"
loading_start: "Start bane"
problem_alert_title: "Ret din kode"
problem_alert_help: "Hjælp"
time_current: "Nu:"
time_total: "Max:"
time_goto: "Gå til:"
non_user_code_problem_title: "Kan ikke indlæse banen"
infinite_loop_title: "Uendelig løkke detekteret"
infinite_loop_description: "Den indledende kode til at bygge verdenen blev aldrig færdig med at køre. Den er sandsynligvis enten meget langsom eller har en uendeligt løkke. Eller også er der en bug. Du kan enten prøve at køre denne kode igen eller nulstille koden til den oprindelige tilstand. Hvis ikke det virker må du meget gerne fortælle os det."
check_dev_console: "Du kan også åbne udviklerkonsollen for at se hvad der kunne være galt."
check_dev_console_link: "(vejledning)"
infinite_loop_try_again: "Prøv igen"
infinite_loop_reset_level: "Nulstil bane"
infinite_loop_comment_out: "Udkommenter min kode"
tip_toggle_play: "Skift mellem afspil/pause med Ctrl+P."
tip_scrub_shortcut: "Brug Ctrl+[ og Ctrl+] til at spole tilbage og frem."
tip_guide_exists: "Klik på guiden i spilmenuen (i toppen af siden) for brugbar info."
tip_open_source: "CodeCombat er 100% open source!"
tip_beta_launch: "CodeCombat søsatte sin beta i oktober, 2013."
tip_think_solution: "Tænk på løsningen, ikke problemet."
tip_theory_practice: "I teorien er der ingen forskel på teori og praksis. Men i praksis er der. - Yogi Bjørn"
tip_error_free: "Der findes to måder at skrive fejlfrie programmer; kun den tredje virker. - Alan Perlis"
tip_debugging_program: "Hvis debugging er at fjerne kodefejl, så må programmering være at proppe fejl ind i koden. - Edsger W. Dijkstra"
tip_forums: "Kig over i vores forum og fortæl os hvad du synes!"
tip_baby_coders: "I fremtiden vil selv babier være Ærketroldmænd."
tip_morale_improves: "Indlæsning vil fortsætte indtil moralen forbedres."
tip_all_species: "Vi tror på lige muligheder for at lære programmering for alle arter."
# tip_reticulating: "Reticulating spines."
# tip_harry: "Yer a Wizard, "
# tip_great_responsibility: "With great coding skill comes great debug responsibility."
tip_harry: "Du' en troldmand, "
tip_great_responsibility: "Med store kodeevner kommer stort fejlfindingsansvnar."
# tip_munchkin: "If you don't eat your vegetables, a munchkin will come after you while you're asleep."
# tip_binary: "There are only 10 types of people in the world: those who understand binary, and those who don't."
# tip_commitment_yoda: "A programmer must have the deepest commitment, the most serious mind. ~ Yoda"

View file

@ -568,6 +568,10 @@
why_paragraph_2_suffix: "That's why CodeCombat is a multiplayer game, not a gamified lesson course. We won't stop until you can't stop--but this time, that's a good thing."
why_paragraph_3: "If you're going to get addicted to some game, get addicted to this one and become one of the wizards of the tech age."
press_title: "Bloggers/Press"
contact_title: "Contact"
codecombat_inc: "CodeCombat, Inc."
address_part_1: "188 King St #507"
address_part_2: "San Francisco, CA 94107"
press_paragraph_1_prefix: "Want to write about us? Feel free to download and use all of the resources included in our"
press_paragraph_1_link: "press packet"
press_paragraph_1_suffix: ". All logos and images may be used without contacting us directly."

View file

@ -170,7 +170,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
accepted: "Aceptado"
rejected: "Rechazado"
withdrawn: "Retirado"
# submitter: "Submitter"
submitter: "Submitter"
submitted: "Enviado"
commit_msg: "Mensaje de Asignación o Commit"
review: "Revisión"
@ -262,9 +262,9 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
victory_hour_of_code_done_yes: "Si, ¡He terminado con mi hora de código!"
victory_experience_gained: "XP Conseguida"
victory_gems_gained: "Gemas Conseguidas"
# victory_new_item: "New Item"
victory_new_item: "Nuevo artículo"
# victory_viking_code_school: "Holy smokes, that was a hard level you just beat! If you aren't already a software developer, you should be. You just got fast-tracked for acceptance with Viking Code School, where you can take your skills to the next level and become a professional web developer in 14 weeks."
# victory_become_a_viking: "Become a Viking"
victory_become_a_viking: "Convertirse en un vikingo"
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.
@ -292,8 +292,8 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
time_current: "Ahora:"
time_total: "Máx:"
time_goto: "Ir a:"
# non_user_code_problem_title: "Unable to Load Level"
# infinite_loop_title: "Infinite Loop Detected"
non_user_code_problem_title: "No puede cargar un nivel"
infinite_loop_title: "Bucle infinito detectado"
# infinite_loop_description: "The initial code to build the world never finished running. It's probably either really slow or has an infinite loop. Or there might be a bug. You can either try running this code again or reset the code to the default state. If that doesn't fix it, please let us know."
# check_dev_console: "You can also open the developer console to see what might be going wrong."
# check_dev_console_link: "(instructions)"
@ -332,7 +332,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
tip_extrapolation: "Existen solo dos clases de personas: aquellos que pueden extrapolar desde información incompleta..."
tip_superpower: "Programar es lo más parecido que tenemos a un superpoder."
tip_control_destiny: "En el verdadero open source, tienes el derecho de controlar tu propio destino. - Linus Torvalds"
# tip_no_code: "No code is faster than no code."
tip_no_code: "Ningún código es más rápido que ningún código"
tip_code_never_lies: "El código nunca os miente, los comentarios algunas veces. — Ron Jeffries"
tip_reusable_software: "Antes de que el software pueda ser reutilizable, primero debe ser utilizable."
tip_optimization_operator: "Cada lenguaje tiene un operator para optimización. En la mayoría de los lenguajes dicho operador es //"
@ -346,7 +346,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
# tip_open_source_contribute: "You can help CodeCombat improve!"
tip_recurse: "Iterar es humano, recursar es divino. - L. Peter Deutsch"
# tip_free_your_mind: "You have to let it all go, Neo. Fear, doubt, and disbelief. Free your mind. - Morpheus"
# tip_strong_opponents: "Even the strongest of opponents always has a weakness. - Itachi Uchiha"
tip_strong_opponents: "Incluso el más fuerte de los opositores oculta debilidad. - Itachi Uchiha"
game_menu:
inventory_tab: "Inventario"
@ -366,14 +366,14 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
auth_caption: "Salvar tu progreso."
leaderboard:
# leaderboard: "Leaderboard"
leaderboard: "Jefe de la liga"
view_other_solutions: "Ver Otras Soluciones" # {change}
scores: "Puntuaciones"
# top_players: "Top Players by"
day: "Hoy"
week: "Esta semana"
# all: "All-Time"
# time: "Time"
time: "Tiempo"
damage_taken: "Daño recibido"
damage_dealt: "Daño causado"
difficulty: "Difficultad"

View file

@ -158,7 +158,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
unwatch: "Ne plus regarder"
submit_patch: "Soumettre un correctif"
submit_changes: "Soumettre des Changements"
# save_changes: "Save Changes"
save_changes: "Sauvegarder les modifications"
general:
and: "et"
@ -263,7 +263,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
victory_experience_gained: "XP gagnée"
victory_gems_gained: "Gemmes gagnées"
victory_new_item: "Nouvel item"
# victory_viking_code_school: "Holy smokes, that was a hard level you just beat! If you aren't already a software developer, you should be. You just got fast-tracked for acceptance with Viking Code School, where you can take your skills to the next level and become a professional web developer in 14 weeks."
victory_viking_code_school: "Par la barbe d'Odin, c'est un niveau difficile que tu viens de compléter! Si tu n'est pas un développeur de logiciel, tu devrais l'être ! Ceci vient de te propulser dans l'école de Code Vikings où tu pourras amener tes habilités au prochain niveau et devenir un développer web profesionnel en deux semaines."
victory_become_a_viking: "Devenez un viking"
guide_title: "Guide"
tome_minion_spells: "Les sorts de vos soldats" # Only in old-style levels.
@ -455,24 +455,24 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
was_free_until: "Vous aviez un abonnement gratuit jusqu'à"
managed_subs: "Gestion des abonnements"
managed_subs_desc: "Ajout d'abonnements pour les autres joueurs (étudiants,enfants,etc.) for other players."
# managed_subs_desc_2: "Recipients must have a CodeCombat account associated with the email address you provide."
# group_discounts: "Group discounts"
# group_discounts_1: "We also offer group discounts for bulk subscriptions."
# group_discounts_1st: "1st subscription"
# group_discounts_full: "Full price"
# group_discounts_2nd: "Subscriptions 2-11"
# group_discounts_20: "20% off"
# group_discounts_12th: "Subscriptions 12+"
# group_discounts_40: "40% off"
# subscribing: "Subscribing..."
# recipient_emails_placeholder: "Enter email address to subscribe, one per line."
# subscribe_users: "Subscribe Users"
# users_subscribed: "Users subscribed:"
# no_users_subscribed: "No users subscribed, please double check your email addresses."
# current_recipients: "Current Recipients"
# unsubscribing: "Unsubscribing..."
# subscribe_prepaid: "Click Subscribe to use prepaid code"
# using_prepaid: "Using prepaid code for monthly subscription"
managed_subs_desc_2: "Recipients must have a CodeCombat account associated with the email address you provide."
group_discounts: "Rabais de groupes"
group_discounts_1: "Nous offrons des rabais de groupe pour les gros abonnements"
group_discounts_1st: "Premier abonnement"
group_discounts_full: "Plein prix"
group_discounts_2nd: "Abonnements 2-11"
group_discounts_20: "Rabais de 20%"
group_discounts_12th: "Abonnements 12+"
group_discounts_40: "Rabais de 40%"
subscribing: "S'inscrit..."
recipient_emails_placeholder: "Entrez votre courriel pour vous abonner, un par ligne."
subscribe_users: "Seulement pour les usagers aboonés"
users_subscribed: "Usagers abonnés:"
no_users_subscribed: "Aucun usager abonnés, veuillez vérifier vos courriels."
current_recipients: "Recipients courant"
unsubscribing: "Desincription en cours..."
subscribe_prepaid: "Cliquer S'abonner pour utiliser du code prépayé"
using_prepaid: "Utiliser le code prépayé pour un abonnement mensuel"
choose_hero:
choose_hero: "Choisissez votre héros"
@ -500,14 +500,14 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
blocks: "Absorbe" # As in "this shield blocks this much damage"
backstab: "Poignardé" # As in "this dagger does this much backstab damage"
skills: "Compétences"
# attack_1: "Deals"
# attack_2: "of listed"
# attack_3: "weapon damage."
# health_1: "Gains"
# health_2: "of listed"
# health_3: "armor health."
# speed_1: "Moves at"
# speed_2: "meters per second."
attack_1: "Inflige"
attack_2: "Classé de"
attack_3: "Dommage causé par l'arme"
health_1: "Gains"
health_2: "Provenance de la liste"
health_3: "Endurance de l'armure"
speed_1: "Se mouvoit à"
speed_2: "mètres par seconde"
available_for_purchase: "Disponible à l'achat" # Shows up when you have unlocked, but not purchased, a hero in the hero store
level_to_unlock: "Niveau à débloquer :" # Label for which level you have to beat to unlock a particular hero (click a locked hero in the store to see)
restricted_to_certain_heroes: "Seulement certains héros peuvent jouer ce niveau."
@ -581,13 +581,13 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
michael_blurb: "Sys Admin"
matt_title: "Programmeur" # {change}
matt_blurb: "Bicycliste"
# cat_title: "Chief Artisan"
# cat_blurb: "Airbender"
# josh_title: "Game Designer"
# josh_blurb: "Floor Is Lava"
# jose_title: "Music"
# jose_blurb: "Taking Off"
# retrostyle_title: "Illustration"
cat_title: "Chef Artisan"
cat_blurb: "Seigneur de l'air"
josh_title: "Designer de jeu"
josh_blurb: "Le plancher est de la lave"
jose_title: "Musique"
jose_blurb: "Décollage"
retrostyle_title: "Illustration"
# retrostyle_blurb: "RetroStyle Games"
teachers:
@ -595,7 +595,7 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
intro_1: "CodeCombat est un jeu en ligne qui enseigne la programmation. Les élèves écrivent du code dans de vrais langages de programmation."
intro_2: "Aucune expérience requise !"
free_title: "Combien cela coûte-t-il ?"
# cost_china: "CodeCombat in China is free for the first five levels, after which it costs $9.99 USD per month for access to our other 140+ levels on our exclusive China servers."
cost_china: "CodeCombat en Chine est gratuit pour les cinq premiers niveaux,après le jeu coûte 9.99$ US par mois pour avoir un accès aux autres 140+ niveaux sur les serveurs exlcusifs chinois"
free_1: "La version de base de CodeCombat est gratuite ! Il y a 70+ niveaux gratuits qui couvrent chaque concepts." # {change}
free_2: "Un abonnement mensuel fournit l'accès à des vidéos de tutoriels ainsi qu'à des niveaux d'entraînement supplémentaires."
teacher_subs_title: "Les enseignants reçoivent un abonnement gratuit !"
@ -609,14 +609,14 @@ module.exports = nativeDescription: "français", englishDescription: "French", t
sub_includes_4: "Support email premium"
sub_includes_5: "7 nouveaux héros avec des capacités uniques à maitriser"
sub_includes_6: "3500 gemmes bonus chaque mois"
# sub_includes_7: "Private Clans"
# monitor_progress_title: "How do I monitor student progress?"
# monitor_progress_1: "Student progress can be monitored by creating a"
# monitor_progress_2: "for your class."
# monitor_progress_3: "To add a student, send them the invite link for your Clan, which is on the"
# monitor_progress_4: "page."
# monitor_progress_5: "After they join, you will see a summary of the student's progress on your Clan's page."
# private_clans_1: "Private Clans provide increased privacy and detailed progress information for each student."
sub_includes_7: "Clans Privées"
monitor_progress_title: "Comment puis-je faire pour surveiller les progrès des étudiants?"
monitor_progress_1: "Le progès des étudiants peut être surveiller en créant un"
monitor_progress_2: "pour votre classe"
monitor_progress_3: "Pour ajouter un étudiant, envoyer leur le lien contenant une invitation pour votre Clan"
monitor_progress_4: "page."
monitor_progress_5: "After they join, you will see a summary of the student's progress on your Clan's page."
private_clans_1: "Private Clans provide increased privacy and detailed progress information for each student."
private_clans_2: "Pour créer un Clan privé, veuillez vous référer à la boîte à cocher 'Faire un clan privé' pendant la création du"
private_clans_3: "."
who_for_title: "A qui CodeCombat est t-il destiné ?"

View file

@ -262,7 +262,7 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese",
victory_hour_of_code_done_yes: "はい、構いません"
victory_experience_gained: "XP獲得"
victory_gems_gained: "ジェム獲得"
# victory_new_item: "New Item"
victory_new_item: "ニューアイテム"
# victory_viking_code_school: "Holy smokes, that was a hard level you just beat! If you aren't already a software developer, you should be. You just got fast-tracked for acceptance with Viking Code School, where you can take your skills to the next level and become a professional web developer in 14 weeks."
# victory_become_a_viking: "Become a Viking"
guide_title: "ガイド"
@ -292,11 +292,11 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese",
time_current: "今:"
time_total: "最大:"
time_goto: "行く:"
# non_user_code_problem_title: "Unable to Load Level"
# infinite_loop_title: "Infinite Loop Detected"
# infinite_loop_description: "The initial code to build the world never finished running. It's probably either really slow or has an infinite loop. Or there might be a bug. You can either try running this code again or reset the code to the default state. If that doesn't fix it, please let us know."
# check_dev_console: "You can also open the developer console to see what might be going wrong."
# check_dev_console_link: "(instructions)"
non_user_code_problem_title: "レベルをロードできません"
infinite_loop_title: "無限ループが見つかりました"
infinite_loop_description: "最初のワールドを作るコードが終わりません。単に遅いか、無限ループになっているかでしょう。バグがあるのかもしれません。再試行してみたり、リセットしてデフォルトに戻すこともできます。もし直せないなら私たちに報告してください。"
check_dev_console: "開発者コンソールをみてなにが間違っているか見ることもできます。"
check_dev_console_link: "(説明書)"
infinite_loop_try_again: "再試行する"
infinite_loop_reset_level: "レベルをリセット"
infinite_loop_comment_out: "マイコードをコメントアウト"
@ -345,8 +345,8 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese",
tip_hate_computers: "コンピュータを憎む人が本当に嫌いなのは下手なプログラマーだ。- ラリー・ニーヴン"
tip_open_source_contribute: "あなたは CodeCombat をより良くすることができます!"
tip_recurse: "繰り返しは人間、再帰は神。 - L・ピーター・ドイツ"
# tip_free_your_mind: "You have to let it all go, Neo. Fear, doubt, and disbelief. Free your mind. - Morpheus"
# tip_strong_opponents: "Even the strongest of opponents always has a weakness. - Itachi Uchiha"
tip_free_your_mind: "全ての雑念を捨てろ、恐怖、疑いも不信も 心を解き放つんだ - モーフィアス"
tip_strong_opponents: "どんな強者にも弱点というものはあるんだ… - うちは イタチ"
game_menu:
inventory_tab: "インベントリー"
@ -405,24 +405,24 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese",
recovered: "前のジェム購入をリカバリーしました。ページを更新してください。"
price: "x3500 / 月"
# subscribe:
# comparison_blurb: "Sharpen your skills with a CodeCombat subscription!"
# feature1: "80+ basic levels across 4 worlds"
# feature2: "7 powerful <strong>new heroes</strong> with unique skills!"
# feature3: "60+ bonus levels"
# feature4: "<strong>3500 bonus gems</strong> every month!"
# feature5: "Video tutorials"
# feature6: "Premium email support"
subscribe:
comparison_blurb: "CodeCombatへ課金してスキルを磨きましょう"
feature1: "80以上の基本レベルがつの世界に"
feature2: "7人のパワフルな <strong>ニューヒーロー</strong> とユニークなスキル!"
feature3: "60以上のボーナスレベル"
feature4: "<strong>3500のジェム</strong>が毎月ボーナス!"
feature5: "ビデオチュートリアル"
feature6: "プレミアムメールサポート"
# feature7: "Private <strong>Clans</strong>"
# free: "Free"
# month: "month"
# subscribe_title: "Subscribe"
# unsubscribe: "Unsubscribe"
free: "無料"
month: ""
subscribe_title: "課金"
unsubscribe: "無課金"
# confirm_unsubscribe: "Confirm Unsubscribe"
# never_mind: "Never Mind, I Still Love You"
# thank_you_months_prefix: "Thank you for supporting us these last"
# thank_you_months_suffix: "months."
# thank_you: "Thank you for supporting CodeCombat."
never_mind: "気にしないでください, それでもあなたが好きです"
thank_you_months_prefix: "私達を "
thank_you_months_suffix: "ヶ月サポートしてくださりありがとうございます。"
thank_you: "CodeCombatをサポートして下さりありがとうございます。"
# sorry_to_see_you_go: "Sorry to see you go! Please let us know what we could have done better."
# unsubscribe_feedback_placeholder: "O, what have we done?"
# parent_button: "Ask your parent"
@ -603,8 +603,8 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese",
teacher_subs_2: "に連絡して無料の月々のサブスクリプションを得ましょう。" # {change}
# teacher_subs_3: "to set up your subscription."
sub_includes_title: "サブスクリプションの内容について"
sub_includes_1: "70以上の基本レベルに加えて、生徒は月々のサブスクリプションを得て次の機能が使えます:" # {change}
sub_includes_2: "40以上の練習レベル" # {change}
sub_includes_1: "80以上の基本レベルに加えて、生徒は月々のサブスクリプションを得て次の機能が使えます:"
sub_includes_2: "60以上の練習レベル"
sub_includes_3: "ビデオチュートリアル"
sub_includes_4: "メールによるサポート"
sub_includes_5: "7人の新しいヒーローとマスターのユニークなスキル"
@ -848,23 +848,23 @@ module.exports = nativeDescription: "日本語", englishDescription: "Japanese",
poll_title: "投票エディター"
back: "バック"
revert: "戻す"
# revert_models: "Revert Models"
revert_models: "モデルを戻す"
pick_a_terrain: "地形を選択してください"
dungeon: "ダンジョン"
indoor: "屋内"
desert: "砂漠"
grassy: "草原"
# small: "Small"
# large: "Large"
# fork_title: "Fork New Version"
# fork_creating: "Creating Fork..."
# generate_terrain: "Generate Terrain"
# more: "More"
small: "小さい"
large: "大きい"
fork_title: "新しいバージョンをフォークする"
fork_creating: "フォークを作成中"
generate_terrain: "地形を生成"
more: "さらに見る"
wiki: "ウィキ"
live_chat: "ライブチャット"
# thang_main: "Main"
# thang_spritesheets: "Spritesheets"
# thang_colors: "Colors"
thang_main: "メイン"
thang_spritesheets: "スプライトシート"
thang_colors: ""
# level_some_options: "Some Options?"
# level_tab_thangs: "Thangs"
# level_tab_scripts: "Scripts"

View file

@ -440,9 +440,9 @@ module.exports = nativeDescription: "Українська", englishDescription:
parents_blurb2: "За 9.99$ на місяць, вона отримуватиме нові завдання щотижня та персональні листи підтримки від професійних програмістів." # {change}
parents_blurb3: "Жодного ризику: 100% гарантія повернення грошей, легке скасування абонементу одним кліком."
payment_methods: "Платіжні методи"
# payment_methods_title: "Accepted Payment Methods"
payment_methods_title: "Платіжні методи, що приймаються"
payment_methods_blurb1: "Наразі ми приймаємо кредитні картник та Alpiay."
# payment_methods_blurb2: "If you require an alternate form of payment, please contact"
payment_methods_blurb2: "Якщо Вам необхідно використати інший спосіб оплати, будь ласка, зв'яжіться з нами."
stripe_description: "Щомісячний абонемент"
subscription_required_to_play: "Аби грати в цьому рівні потрібен абонемент."
unlock_help_videos: "Підпишіться, щоб відкрити усі навчальні відео."
@ -455,7 +455,7 @@ module.exports = nativeDescription: "Українська", englishDescription:
was_free_until: "У Вас був безкоштовний абонемент до "
managed_subs: "Керовані абонементи"
managed_subs_desc: "Додати абонементи для інших гравців (учнів, дітей тощо)"
# managed_subs_desc_2: "Recipients must have a CodeCombat account associated with the email address you provide."
managed_subs_desc_2: "Одержувачі повинні мати обліковий запис CodeCombat, пов'язаний з вказаною Вами адресою електронної пошти."
group_discounts: "Групові знижки"
group_discounts_1: "Ми також пропонуємо знижки для пакетних передплат."
group_discounts_1st: "1-ий абонемент (включає Ваш)" # {change}
@ -592,8 +592,8 @@ module.exports = nativeDescription: "Українська", englishDescription:
teachers:
title: "CodeCombat для вчителів" # {change}
# intro_1: "CodeCombat is an online game that teaches programming. Students write code in real programming languages."
# intro_2: "No experience required!"
intro_1: "CodeCombat - це онлайн гра, що вчить програмуванню. Студенти пишуть код на реальних мовах програмування."
intro_2: "Досвід не потрібен!"
free_title: "Скільки це коштує?"
# cost_china: "CodeCombat in China is free for the first five levels, after which it costs $9.99 USD per month for access to our other 140+ levels on our exclusive China servers."
# free_1: "There are 80+ FREE levels which cover every concept."
@ -626,17 +626,17 @@ module.exports = nativeDescription: "Українська", englishDescription:
# material_china: "Approximately 30 hours of gameplay spread over 140+ subscriber-only levels so far, with new levels every week."
# material_1: "Approximately 10 hours of free content and an additional 20 hours of subscriber content, with new levels every week."
# concepts_title: "What concepts are covered?"
# how_much_title: "How much does a monthly subscription cost?"
# how_much_1: "A"
# how_much_2: "monthly subscription"
# how_much_3: "costs $9.99, and can be cancelled anytime."
# how_much_4: "Additionally, we provide discounts for larger groups:"
# how_much_5: "We accept discounted one-time purchases and yearly subscription purchases for groups, such as a class or school. Please contact"
# how_much_6: "for more details."
# more_info_title: "Where can I find more information?"
# more_info_1: "Our"
# more_info_2: "teachers forum"
# more_info_3: "is a good place to connect with fellow educators who are using CodeCombat."
how_much_title: "Скільки коштує місячна передплата?"
how_much_1: ""
how_much_2: "Місячна передплата"
how_much_3: "коштує $9.99, та може бути скасована будь-коли."
how_much_4: "Крім цього, ми надаємо знижки для великих груп:"
how_much_5: "Ми надаємо знижку на разові закупівлі та річну передплату для груп, таких як клас або школа. Будь ласка, зв'яжіться з нами"
how_much_6: "для отримання більш детальної інформації."
more_info_title: "Де я можу знайти більше інформації?"
more_info_1: "Наш"
more_info_2: "вчительський форум"
more_info_3: "є гарним місцем для спілкування із колегами-педагогами, котрі використовують CodeCombat."
sys_requirements_title: "Системні вимоги"
sys_requirements_1: "Оскільки CodeCombat — це гра, для нормальної роботи він вимагає у комп'ютерів більше, ніж відео чи текстові посібники. Ми оптимізували його для швидкої роботи в усіх сучасних браузерах і на старіших машинах, щоб кожен міг грати. І ось наші підказки, як отримати від CodeCombat якнайбільше:" # {change}
sys_requirements_2: "Використовуйте новіші версії Chrome або Firefox." # {change}
@ -782,27 +782,27 @@ module.exports = nativeDescription: "Українська", englishDescription:
new_name: "Назва нового клану"
new_description: "Опис нового клану"
make_private: "Зробити клан приватним"
# subs_only: "subscribers only"
subs_only: "лише для підписчиків"
create_clan: "Створити новий клан"
public_clans: "Публічні клани"
my_clans: "Мої клани"
clan_name: "Назва клану"
# name: "Name"
# chieftain: "Chieftain"
name: "Ім'я"
chieftain: "Отаман"
type: "Тип"
edit_clan_name: "Змінити назву клану"
edit_clan_description: "Змінити опис клану"
# edit_name: "edit name"
# edit_description: "edit description"
# private: "(private)"
edit_name: "Змінити ім'я"
edit_description: "Змінити опис"
private: "(закритий)"
summary: "Загалом"
average_level: "Середній рівень"
# average_achievements: "Average Achievements"
delete_clan: "Видалити калн"
average_achievements: "Середні досягнення"
delete_clan: "Видалити клан"
leave_clan: "Покинути клан"
join_clan: "Приєднатись до клану"
invite_1: "Запрошення:"
invite_2: "*Запросіть гравців до цього Клану виславши дане посилання."
invite_2: "*Запросіть гравців до цього Клану, виславши дане посилання."
members: "Учасники"
progress: "Поступ"
not_started_1: "не розпочато"

View file

@ -47,7 +47,20 @@ block content
a(href="https://s3.amazonaws.com/CodeCombatMisc/press_packet.zip", data-i18n="about.press_paragraph_1_link") press packet
span(data-i18n="about.press_paragraph_1_suffix")
| . All logos and images may be used without contacting us directly.
h2(data-i18n="about.contact_title")
| Contact
p
span(data-i18n="about.codecombat_inc")
| CodeCombat, Inc.
br
span(data-i18n="about.address_part_1")
| 188 King St #507
br
span(data-i18n="about.address_part_2")
| San Francisco, CA 94107
br
a(href='mailto:team@codecombat.com') team@codecombat.com
ul.col-sm-6.team-column
@ -79,9 +92,11 @@ block content
li.row
img(src="/images/pages/about/george_small.png").img-thumbnail
a(href="http://www.georgesaines.com/")
img(src="/images/pages/about/george_small.png").img-thumbnail
.team_bio
h4.team_name George Saines
h4.team_name
a(href="http://www.georgesaines.com/") George Saines
p(data-i18n="about.george_title")
| Cofounder
p(data-i18n="about.george_blurb")
@ -130,7 +145,7 @@ block content
| Floor Is Lava
a(href="https://soundcloud.com/taking-off")
img(src="/images/pages/about/placeholder.png").img-thumbnail
img(src="/images/pages/about/jose_small.png").img-thumbnail
.team_bio
h4.team_name
a(href="https://soundcloud.com/taking-off") Jose Antonini
@ -142,7 +157,7 @@ block content
li.row
a(href="http://retrostylegames.com/")
img(src="/images/pages/about/placeholder.png").img-thumbnail
img(src="/images/pages/about/pavel_small.png").img-thumbnail
.team_bio
h4.team_name
a(href="http://retrostylegames.com/") Pavel Konstantinov
@ -152,7 +167,7 @@ block content
| RetroStyle Games
a(href="http://retrostylegames.com/")
img(src="/images/pages/about/placeholder.png").img-thumbnail
img(src="/images/pages/about/oleg_small.png").img-thumbnail
.team_bio
h4.team_name
a(href="http://retrostylegames.com/") Oleg Ulyanickiy

View file

@ -73,7 +73,7 @@ block append content
tr
td
a(href="/clans/#{clan.id}")= clan.get('name')
td
td
if idNameMap && idNameMap[clan.get('ownerID')]
a(href="/user/#{clan.get('ownerID')}")= idNameMap[clan.get('ownerID')]
else
@ -160,7 +160,7 @@ block append content
each achievement, index in earnedAchievements.models
tr(class=index > 4 ? 'hide' : '')
td= achievement.get('achievementName')
td= moment().format("MMMM Do YYYY", achievement.get('changed'))
td= moment(achievement.get('changed')).format("MMMM Do YYYY")
if achievement.get('achievedAmount')
td= achievement.get('achievedAmount')
else

View file

@ -306,7 +306,7 @@ class RecipientSubs
options = { cache: false, url: "/db/user/#{me.id}/stripe" }
options.success = (info) =>
@sponsorSub = info.subscription
@sponsorSub = info.sponsorSubscription
if card = info.card
@card = "#{card.brand}: x#{card.last4}"
render()

View file

@ -121,7 +121,7 @@ module.exports = class DiplomatView extends ContributeClassView
vi: ['An Nguyen Hoang Thien'] # Tiếng Việt, Vietnamese
hu: ['Anon', 'atlantisguru', 'bbeasmile', 'csuvsaregal', 'divaDseidnA', 'ferpeter', 'kinez'] # magyar, Hungarian
th: ['Kamolchanok Jittrepit'] # , Thai
da: ['Anon', 'Einar Rasmussen', 'Rahazan', 'Randi Hillerøe', 'Silwing', 'marc-portier', 'sorsjen'] # dansk, Danish
da: ['Anon', 'Einar Rasmussen', 'Rahazan', 'Randi Hillerøe', 'Silwing', 'marc-portier', 'sorsjen', 'Zleep-Dogg'] # dansk, Danish
ko: ['Melondonut'] # , Korean
sk: ['Anon', 'Juraj Pecháč'] # slovenčina, Slovak
sl: [] # slovenščina, Slovene

View file

@ -70,6 +70,7 @@ defaultTasks =
'Add other Components like Shoots or Casts if needed.'
'Configure other Components, like Moves, Attackable, Attacks, etc.'
'Override the HasAPI type if it will not be correctly inferred.'
'Add to Existence System power table.'
]
Hero: commonTasks.concat animatedThangTypeTasks.concat purchasableTasks.concat [
'Set the hero class.'

View file

@ -265,7 +265,6 @@ module.exports = class CampaignView extends RootView
level.locked = false if @editorMode
level.locked = false if @campaign?.get('name') is 'Auditions'
level.locked = false if me.isInGodMode()
level.locked = false if level.slug is 'apocalypse'
level.disabled = true if level.adminOnly and @levelStatusMap[level.slug] not in ['started', 'complete']
level.disabled = false if me.isInGodMode()
level.color = 'rgb(255, 80, 60)'
@ -306,7 +305,7 @@ module.exports = class CampaignView extends RootView
if nextLevel and not nextLevel.locked and not nextLevel.disabled and @levelStatusMap[nextLevel.slug] isnt 'complete' and nextLevel.slug not in dontPointTo and not nextLevel.replayable and (
me.isPremium() or
not nextLevel.requiresSubscription or
nextLevel.slug is 'apocalypse' or
(nextLevel.slug is 'boom-and-bust' and not @levelStatusMap['defense-of-plainswood']) or
(nextLevel.slug is 'favorable-odds' and not @levelStatusMap['the-raised-sword'])
)
nextLevel.next = true
@ -352,9 +351,9 @@ module.exports = class CampaignView extends RootView
particleKey.push level.type if level.type and level.type isnt 'hero'
particleKey.push 'replayable' if level.replayable
particleKey.push 'premium' if level.requiresSubscription
particleKey.push 'gate' if level.slug in ['kithgard-gates', 'siege-of-stonehold', 'clash-of-clones']
particleKey.push 'gate' if level.slug in ['kithgard-gates', 'siege-of-stonehold', 'clash-of-clones', 'summits-gate']
particleKey.push 'hero' if level.unlocksHero and not level.unlockedHero
particleKey.push 'item' if level.slug is 'apocalypse' # TODO: generalize
#particleKey.push 'item' if level.slug is 'apocalypse' # TODO: generalize
continue if particleKey.length is 2 # Don't show basic levels
continue unless level.hidden or _.intersection(particleKey, ['item', 'hero-ladder', 'replayable']).length
@particleMan.addEmitter level.position.x / 100, level.position.y / 100, particleKey.join('-')

View file

@ -329,7 +329,7 @@ module.exports = class HeroVictoryModal extends ModalView
AudioPlayer.playSound name, 1
getNextLevelCampaign: ->
{'kithgard-gates': 'forest', 'siege-of-stonehold': 'desert', 'clash-of-clones': 'mountain'}[@level.get('slug')] or @level.get 'campaign' # Much easier to just keep this updated than to dynamically figure it out.
{'kithgard-gates': 'forest', 'kithgard-mastery': 'forest', 'siege-of-stonehold': 'desert', 'clash-of-clones': 'mountain'}[@level.get('slug')] or @level.get 'campaign' # Much easier to just keep this updated than to dynamically figure it out.
getNextLevelLink: ->
link = '/play'

View file

@ -616,7 +616,7 @@ module.exports = class InventoryModal extends ModalView
for slot, original of equipment
item = _.find @items.models, (item) -> item.get('original') is original
continue unless dollImages = item?.get('dollImages')
didAdd = @addDollImage slot, dollImages, heroClass, gender
didAdd = @addDollImage slot, dollImages, heroClass, gender, item
slotsWithImages.push slot if didAdd if item.get('original') isnt '54ea39342b7506e891ca70f2' # Circlet of the Magi needs hair under it
@$el.find('#hero-image-hair').toggle not ('head' in slotsWithImages)
@$el.find('#hero-image-thumb').toggle not ('gloves' in slotsWithImages)
@ -626,7 +626,7 @@ module.exports = class InventoryModal extends ModalView
removeDollImages: ->
@$el.find('.doll-image').remove()
addDollImage: (slot, dollImages, heroClass, gender) ->
addDollImage: (slot, dollImages, heroClass, gender, item) ->
heroClass = @selectedHero?.get('heroClass') ? 'Warrior'
gender = if @selectedHero?.get('slug') in heroGenders.male then 'male' else 'female'
didAdd = false
@ -637,6 +637,9 @@ module.exports = class InventoryModal extends ModalView
imageKeys = ["#{gender}", "#{gender}Thumb"]
else if heroClass is 'Wizard' and slot is 'torso'
imageKeys = [gender, "#{gender}Back"]
else if heroClass is 'Ranger' and slot is 'head' and item.get('original') in ['5441c2be4e9aeb727cc97105', '5441c3144e9aeb727cc97111']
# All-class headgear like faux fur hat, viking helmet is abusing ranger glove slot
imageKeys = ["#{gender}Ranger"]
else
imageKeys = [gender]
for imageKey in imageKeys

View file

@ -0,0 +1,136 @@
// Average level playtimes by campaign
// Usage:
// mongo <address>:<port>/<database> <script file> -u <username> -p <password>
// NOTE: faster to use find() instead of aggregate()
// NOTE: faster to ask for one level at a time. also keeps levels in campaign order
// Excluded for one reason or another
// Some relevant code: https://github.com/codecombat/codecombat/blob/master/app/views/play/CampaignView.coffee#L281-L292
var excludedLevels = ['deadly-dungeon-rescue', 'kithgard-brawl', 'cavern-survival', 'kithgard-mastery', 'destroying-angel', 'kithgard-apprentice', 'wild-horses', 'lost-viking', 'forest-flower-grove', 'boulder-woods', 'the-trials'];
var scriptStartTime = new Date();
var startDay = '2015-05-10';
var endDay = '2015-06-11';
log("Dates: " + startDay + " to " + endDay);
// Print out playtimes for each campaign
var campaigns = getCampaigns();
for (var i = 0; i < campaigns.length; i++) {
var campaign = campaigns[i];
// if (campaign.slug !== 'dungeon') continue;
print(campaign.slug + " (free)");
var total = 0;
for (var j = 0; j < campaign.free.length; j++) {
var levelSlug = campaign.free[j];
if (excludedLevels.indexOf(levelSlug) >= 0) continue;
var data = getPlaytimes([levelSlug]);
print(data[levelSlug].average + "\t" + data[levelSlug].count + "\t" + levelSlug);
total += data[levelSlug];
}
// print(parseInt(total/60/60) + "\t\t total hours");
total = 0;
print(campaign.slug + " (paid)");
for (var j = 0; j < campaign.paid.length; j++) {
var levelSlug = campaign.paid[j];
if (excludedLevels.indexOf(levelSlug) >= 0) continue;
var data = getPlaytimes([levelSlug]);
if (data[levelSlug]) {
print(data[levelSlug].average + "\t" + data[levelSlug].count + "\t" + levelSlug);
total += data[levelSlug];
}
else {
print("0\t0\t" + levelSlug);
}
}
// print(parseInt(total/60/60) + "\t\t total hours");
total = 0;
print(campaign.slug + " (replayable)");
for (var j = 0; j < campaign.replayable.length; j++) {
var levelSlug = campaign.replayable[j];
if (excludedLevels.indexOf(levelSlug) >= 0) continue;
var data = getPlaytimes([levelSlug]);
print(data[levelSlug].average + "\t" + data[levelSlug].count + "\t" + levelSlug);
total += data[levelSlug];
}
// print(parseInt(total/60/60) + "\t\t total hours");
// break;
}
log("Script runtime: " + (new Date() - scriptStartTime));
function log(str) {
print(new Date().toISOString() + " " + str);
}
function objectIdWithTimestamp(timestamp) {
// Convert string date to Date object (otherwise assume timestamp is a date)
if (typeof(timestamp) == 'string') timestamp = new Date(timestamp);
// Convert date object to hex seconds since Unix epoch
var hexSeconds = Math.floor(timestamp/1000).toString(16);
// Create an ObjectId with that hex timestamp
var constructedObjectId = ObjectId(hexSeconds + "0000000000000000");
return constructedObjectId
}
function getCampaigns() {
var campaigns = [];
var cursor = db.campaigns.find({}, {slug: 1, levels: 1});
var allFree = 0;
var allpaid = 0;
while (cursor.hasNext()) {
var doc = cursor.next();
if (doc.slug === 'auditions') continue;
var campaign = {slug: doc.slug, free: [], paid: [], replayable: []};
for (var levelID in doc.levels) {
if (doc.levels[levelID].replayable) {
campaign.replayable.push(doc.levels[levelID].slug);
}
else if (doc.levels[levelID].requiresSubscription) {
campaign.paid.push(doc.levels[levelID].slug);
}
else {
campaign.free.push(doc.levels[levelID].slug);
}
}
campaigns.push(campaign);
}
return campaigns;
}
function getPlaytimes(levelSlugs) {
var startObj = objectIdWithTimestamp(ISODate(startDay + "T00:00:00.000Z"));
var endObj = objectIdWithTimestamp(ISODate(endDay + "T00:00:00.000Z"))
var cursor = db['level.sessions'].find({
$and:
[
{"state.complete": true},
{"playtime": {$gt: 0}},
{levelID: {$in: levelSlugs}},
{_id: {$gte: startObj}},
{_id: {$lt: endObj}}
]
});
var playtimes = {};
while (cursor.hasNext()) {
var myDoc = cursor.next();
var levelID = myDoc.levelID;
if (!playtimes[levelID]) playtimes[levelID] = [];
playtimes[levelID].push(myDoc.playtime);
}
var data = {};
for (levelID in playtimes) {
var total = playtimes[levelID].reduce(function(a, b) {return a + b;});
data[levelID] = {count: playtimes[levelID].length, total: total};
data[levelID]['average'] = parseInt(total / playtimes[levelID].length);
}
return data;
}

View file

@ -0,0 +1,153 @@
// Level completion counts broken down into free and paid buckets
// Usage:
// mongo <address>:<port>/<database> <script file> -u <username> -p <password>
// TODO: subscriber is someone who is currently subscribed, not necessarily subscribed when they completed a level
// Excluded for one reason or another
// Some relevant code: https://github.com/codecombat/codecombat/blob/master/app/views/play/CampaignView.coffee#L281-L292
var excludedLevels = ['deadly-dungeon-rescue', 'kithgard-brawl', 'cavern-survival', 'kithgard-mastery', 'destroying-angel', 'kithgard-apprentice', 'wild-horses', 'lost-viking', 'forest-flower-grove', 'boulder-woods', 'the-trials'];
var scriptStartTime = new Date();
var startDay = '2015-05-16';
var endDay = '2015-06-17';
log("Dates: " + startDay + " to " + endDay);
var subscribers = getSubscribers();
log("Subscriber count: " + Object.keys(subscribers).length);
var campaigns = getCampaigns();
for (var i = 0; i < campaigns.length; i++) {
var campaign = campaigns[i];
// if (campaign.slug !== 'mountain') continue;
function printCampaign(title, prop) {
print(title)
print("Total\tFree\tSubscribers");
for (var j = 0; j < campaign[prop].length; j++) {
var levelSlug = campaign[prop][j];
if (excludedLevels.indexOf(levelSlug) >= 0) continue;
var data = getCompletionCounts([levelSlug], subscribers);
if (data[levelSlug]) {
var free = data[levelSlug].free.length;
var paid = data[levelSlug].paid.length;
var total = free + paid;
var paidRate = parseInt(paid / total * 100);
print(total + "\t" + free + "\t" + paid + "\t\t" + paidRate + "%\t" + levelSlug);
}
else {
print("0\t0\t0\t\t0%\t" + levelSlug);
}
}
}
printCampaign(campaign.slug + " (free)", "free");
printCampaign(campaign.slug + " (paid)", "paid");
printCampaign(campaign.slug + " (replayable)", "replayable");
// break;
}
log("Script runtime: " + (new Date() - scriptStartTime));
function log(str) {
print(new Date().toISOString() + " " + str);
}
function objectIdWithTimestamp(timestamp) {
// Convert string date to Date object (otherwise assume timestamp is a date)
if (typeof(timestamp) == 'string') timestamp = new Date(timestamp);
// Convert date object to hex seconds since Unix epoch
var hexSeconds = Math.floor(timestamp/1000).toString(16);
// Create an ObjectId with that hex timestamp
var constructedObjectId = ObjectId(hexSeconds + "0000000000000000");
return constructedObjectId
}
function getCampaigns() {
var campaigns = [];
var cursor = db.campaigns.find({}, {slug: 1, levels: 1});
var allFree = 0;
var allpaid = 0;
while (cursor.hasNext()) {
var doc = cursor.next();
if (doc.slug === 'auditions') continue;
var campaign = {slug: doc.slug, free: [], paid: [], replayable: []};
for (var levelID in doc.levels) {
if (doc.levels[levelID].replayable) {
campaign.replayable.push(doc.levels[levelID].slug);
}
else if (doc.levels[levelID].requiresSubscription) {
campaign.paid.push(doc.levels[levelID].slug);
}
else {
campaign.free.push(doc.levels[levelID].slug);
}
}
campaigns.push(campaign);
}
return campaigns;
}
function getCompletionCounts(levelSlugs, subscribers) {
var startObj = objectIdWithTimestamp(ISODate(startDay + "T00:00:00.000Z"));
var endObj = objectIdWithTimestamp(ISODate(endDay + "T00:00:00.000Z"))
var cursor = db['level.sessions'].find({
$and:
[
{"state.complete": true},
{levelID: {$in: levelSlugs}},
{_id: {$gte: startObj}},
{_id: {$lt: endObj}}
]
});
var completionCounts = {};
while (cursor.hasNext()) {
var myDoc = cursor.next();
var userID = myDoc.creator;
var levelID = myDoc.levelID;
if (!completionCounts[levelID]) completionCounts[levelID] = {free: [], paid: []};
if (subscribers[userID]) {
completionCounts[levelID].paid.push(myDoc._id.valueOf());
}
else {
completionCounts[levelID].free.push(myDoc._id.valueOf());
}
}
return completionCounts;
}
function getSubscribers() {
var cursor = db['users'].find({
$and:
[
{
$or:
[
{"stripe.sponsorID": {$exists: true}},
{$and:
[
{"stripe.subscriptionID": {$exists: true}},
{"stripe.planID": 'basic'}
]
}
]
},
{permissions: {$ne: ['admin']}},
{"stripe.free": {$exists: false}},
{"stripe.coupon": {$exists: false}},
{"stripe.prepaidCode": {$exists: false}}
]
});
var subscribers = {};
while (cursor.hasNext()) {
subscribers[cursor.next()._id.valueOf()] = true;
}
return subscribers;
}

View file

@ -23,5 +23,8 @@ LevelSystemHandler = class LevelSystemHandler extends Handler
hasAccess: (req) ->
req.method is 'GET' or req.user?.isAdmin() or req.user?.isArtisan()
hasAccessToDocument: (req, document, method) ->
if req.user?.isArtisan() then true else super req, document, method
module.exports = new LevelSystemHandler()

View file

@ -582,18 +582,26 @@ class SubscriptionHandler extends Handler
email = req.body.stripe.unsubscribeEmail.trim().toLowerCase()
return done({res: 'Database error.', code: 500}) if _.isEmpty(email)
deleteUserStripeProp = (user, propName) ->
stripeInfo = _.cloneDeep(user.get('stripe') ? {})
delete stripeInfo[propName]
if _.isEmpty stripeInfo
user.set 'stripe', undefined
else
user.set 'stripe', stripeInfo
User.findOne {emailLower: email}, (err, recipient) =>
if err
@logSubscriptionError(user, "User lookup error. " + err)
return done({res: 'Database error.', code: 500})
unless recipient
@logSubscriptionError(user, "Recipient #{req.body.stripe.recipient} not found. " + err)
@logSubscriptionError(user, "Recipient #{email} not found.")
return done({res: 'Database error.', code: 500})
# Check recipient is currently sponsored
stripeRecipient = recipient.get 'stripe' ? {}
if stripeRecipient?.sponsorID isnt user.id
@logSubscriptionError(user, "Recipient #{req.body.stripe.recipient} not found. " + err)
@logSubscriptionError(user, "Recipient #{req.body.stripe.recipient} not found. ")
return done({res: 'Can only unsubscribe sponsored subscriptions.', code: 403})
# Find recipient subscription
@ -603,22 +611,41 @@ class SubscriptionHandler extends Handler
sponsoredEntry = sponsored
break
unless sponsoredEntry?
@logSubscriptionError(user, 'Unable to find sponsored subscription. ' + err)
@logSubscriptionError(user, 'Unable to find recipient subscription. ')
return done({res: 'Database error.', code: 500})
# Cancel Stripe subscription
stripe.customers.cancelSubscription stripeInfo.customerID, sponsoredEntry.subscriptionID, { at_period_end: true }, (err) =>
if err or not recipient
@logSubscriptionError(user, "Stripe cancel sponsored subscription failed. " + err)
# Update recipient user
deleteUserStripeProp(recipient, 'sponsorID')
recipient.save (err) =>
if err
@logSubscriptionError(user, 'Recipient user save unsubscribe error. ' + err)
return done({res: 'Database error.', code: 500})
delete stripeInfo.unsubscribeEmail
user.set('stripe', stripeInfo)
req.body.stripe = stripeInfo
user.save (err) =>
# Cancel Stripe subscription
stripe.customers.cancelSubscription stripeInfo.customerID, sponsoredEntry.subscriptionID, (err) =>
if err
@logSubscriptionError(user, 'User save unsubscribe error. ' + err)
@logSubscriptionError(user, "Stripe cancel sponsored subscription failed. " + err)
return done({res: 'Database error.', code: 500})
done()
# Update sponsor user
_.remove(stripeInfo.recipients, (s) -> s.userID is recipient.id)
delete stripeInfo.unsubscribeEmail
user.set('stripe', stripeInfo)
req.body.stripe = stripeInfo
user.save (err) =>
if err
@logSubscriptionError(user, 'Sponsor user save unsubscribe error. ' + err)
return done({res: 'Database error.', code: 500})
return done() unless stripeInfo.sponsorSubscriptionID?
# Update sponsored subscription quantity
options =
quantity: getSponsoredSubsAmount(subscriptions.basic.amount, stripeInfo.recipients.length, stripeInfo.subscriptionID?)
stripe.customers.updateSubscription stripeInfo.customerID, stripeInfo.sponsorSubscriptionID, options, (err, subscription) =>
if err
logStripeWebhookError(err)
return res.send(500, '')
done()
module.exports = new SubscriptionHandler()

View file

@ -157,6 +157,10 @@ module.exports.setup = (app) ->
unless recipient
logStripeWebhookError("Recipient not found #{subscription.metadata.id}")
return res.send(500, '')
# Recipient cancellations are immediate, no work to perform if recipient's sponsorID is already gone
return res.send(200, '') unless recipient.get('stripe')?.sponsorID?
User.findById recipient.get('stripe').sponsorID, (err, sponsor) =>
if err
logStripeWebhookError(err)

View file

@ -259,6 +259,8 @@ describe 'Subscriptions', ->
# console.log 'verifyNotSponsoring', sponsorID, recipientID
User.findById sponsorID, (err, sponsor) ->
expect(err).toBeNull()
expect(sponsor).not.toBeNull()
return done() unless sponsor
stripeInfo = sponsor.get('stripe')
return done() unless stripeInfo?.customerID?
checkSubscriptions = (starting_after, done) ->
@ -282,6 +284,7 @@ describe 'Subscriptions', ->
User.findById sponsorUserID, (err, user) ->
expect(err).toBeNull()
expect(user).not.toBeNull()
return done() unless user
sponsorStripe = user.get('stripe')
sponsorCustomerID = sponsorStripe.customerID
numSponsored = sponsorStripe.recipients?.length
@ -443,7 +446,7 @@ describe 'Subscriptions', ->
expect(err?).toEqual(false)
done(updatedUser)
unsubscribeRecipient = (sponsor, recipient, immediately, done) ->
unsubscribeRecipient = (sponsor, recipient, done) ->
# console.log 'unsubscribeRecipient', sponsor.id, recipient.id
stripeInfo = sponsor.get('stripe')
customerID = stripeInfo.customerID
@ -467,20 +470,7 @@ describe 'Subscriptions', ->
request.put {uri: userURL, json: requestBody, headers: headers }, (err, res, body) ->
expect(err).toBeNull()
expect(res.statusCode).toBe(200)
# Simulate subscription ending after cancellation
return done() unless immediately
# Simulate subscription cancelling at period end
stripe.customers.cancelSubscription customerID, subscriptionID, (err) ->
expect(err).toBeNull()
# Simulate customer.subscription.deleted webhook event
event = _.cloneDeep(customerSubscriptionDeletedSampleEvent)
event.data.object = subscription
request.post {uri: webhookURL, json: event}, (err, res, body) ->
expect(err).toBeNull()
done()
done()
# Subscribe a bunch of recipients at once, used for bulk discount testing
class SubbedRecipients
@ -762,11 +752,11 @@ describe 'Subscriptions', ->
expect(err).toBeNull()
User.findById user1.id, (err, user1) ->
unsubscribeRecipient user1, user2, true, ->
unsubscribeRecipient user1, user2, ->
User.findById user1.id, (err, user1) ->
expect(err).toBeNull()
expect(user1.get('stripe').subscriptionID).toBeDefined()
expect(user1.get('stripe').recipients).toBeUndefined()
expect(_.isEmpty(user1.get('stripe').recipients)).toEqual(true)
expect(user1.isPremium()).toEqual(true)
User.findById user2.id, (err, user2) ->
verifyNotSponsoring user1.id, user2.id, ->
@ -781,7 +771,7 @@ describe 'Subscriptions', ->
loginNewUser (user1) ->
subscribeRecipients user1, [user2], token, (updatedUser) ->
User.findById user1.id, (err, user1) ->
unsubscribeRecipient user1, user2, true, ->
unsubscribeRecipient user1, user2, ->
verifyNotSponsoring user1.id, user2.id, ->
verifyNotRecipient user2.id, done
@ -793,7 +783,7 @@ describe 'Subscriptions', ->
loginNewUser (user1) ->
subscribeRecipients user1, [user2], token, (updatedUser) ->
User.findById user1.id, (err, user1) ->
unsubscribeRecipient user1, user2, false, ->
unsubscribeRecipient user1, user2, ->
subscribeRecipients user1, [user2], null, (updatedUser) ->
verifySponsorship user1.id, user2.id, done
@ -846,7 +836,7 @@ describe 'Subscriptions', ->
expect(err).toBeNull()
subscribeRecipients user1, [user2], null, (updatedUser) ->
User.findById user1.id, (err, user1) ->
unsubscribeRecipient user1, user2, true, ->
unsubscribeRecipient user1, user2, ->
User.findById user1.id, (err, user1) ->
expect(err).toBeNull()
expect(user1.get('stripe').subscriptionID).toBeDefined()
@ -1138,7 +1128,7 @@ describe 'Subscriptions', ->
User.findById user1.id, (err, user1) ->
# Unsubscribe recipient0
unsubscribeRecipient user1, recipients.get(0), true, ->
unsubscribeRecipient user1, recipients.get(0), ->
User.findById user1.id, (err, user1) ->
stripeInfo = user1.get('stripe')
expect(stripeInfo.recipients.length).toEqual(1)
@ -1150,7 +1140,7 @@ describe 'Subscriptions', ->
expect(subscription.quantity).toEqual(getSubscribedQuantity(1))
# Unsubscribe recipient1
unsubscribeRecipient user1, recipients.get(1), true, ->
unsubscribeRecipient user1, recipients.get(1), ->
User.findById user1.id, (err, user1) ->
stripeInfo = user1.get('stripe')
expect(stripeInfo.recipients.length).toEqual(0)
@ -1186,7 +1176,7 @@ describe 'Subscriptions', ->
User.findById user1.id, (err, user1) ->
# Unsubscribe first recipient
unsubscribeRecipient user1, recipients.get(0), true, ->
unsubscribeRecipient user1, recipients.get(0), ->
User.findById user1.id, (err, user1) ->
stripeInfo = user1.get('stripe')
expect(stripeInfo.recipients.length).toEqual(recipientCount - 1)
@ -1198,7 +1188,7 @@ describe 'Subscriptions', ->
expect(subscription.quantity).toEqual(getSubscribedQuantity(recipientCount - 1))
# Unsubscribe second recipient
unsubscribeRecipient user1, recipients.get(1), true, ->
unsubscribeRecipient user1, recipients.get(1), ->
User.findById user1.id, (err, user1) ->
stripeInfo = user1.get('stripe')
expect(stripeInfo.recipients.length).toEqual(recipientCount - 2)
@ -1218,7 +1208,7 @@ describe 'Subscriptions', ->
# Unsubscribe third recipient
verifySponsorship user1.id, recipients.get(2).id, ->
unsubscribeRecipient user1, recipients.get(2), true, ->
unsubscribeRecipient user1, recipients.get(2), ->
User.findById user1.id, (err, user1) ->
stripeInfo = user1.get('stripe')
expect(stripeInfo.recipients.length).toEqual(recipientCount - 3)
@ -1252,8 +1242,9 @@ describe 'Subscriptions', ->
User.findById user1.id, (err, user1) ->
# Unsubscribe first recipient
unsubscribeRecipient user1, recipients.get(0), true, ->
unsubscribeRecipient user1, recipients.get(0), ->
User.findById user1.id, (err, user1) ->
stripeInfo = user1.get('stripe')
expect(stripeInfo.recipients.length).toEqual(recipientCount - 1)
verifyNotSponsoring user1.id, recipients.get(0).id, ->
@ -1264,7 +1255,7 @@ describe 'Subscriptions', ->
expect(subscription.quantity).toEqual(getUnsubscribedQuantity(recipientCount - 1))
# Unsubscribe last recipient
unsubscribeRecipient user1, recipients.get(recipientCount - 1), true, ->
unsubscribeRecipient user1, recipients.get(recipientCount - 1), ->
User.findById user1.id, (err, user1) ->
stripeInfo = user1.get('stripe')
expect(stripeInfo.recipients.length).toEqual(recipientCount - 2)