Merge branch 'master' into feature/bootstrap3

This commit is contained in:
Scott Erickson 2014-01-27 11:03:04 -08:00
commit 88c8c3896b
49 changed files with 1515 additions and 339 deletions

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,7 @@
CocoClass = require 'lib/CocoClass'
{me, CURRENT_USER_KEY} = require 'lib/auth'
{backboneFailure} = require 'lib/errors'
{saveObjectToStorage} = require 'lib/storage'
storage = require 'lib/storage'
# facebook user object props to
userPropsToSave =
@ -59,7 +59,7 @@ module.exports = FacebookHandler = class FacebookHandler extends CocoClass
error: backboneFailure,
url: "/db/user?facebookID=#{r.id}&facebookAccessToken=#{@authResponse.accessToken}"
success: (model) ->
saveObjectToStorage(CURRENT_USER_KEY, model.attributes)
storage.save(CURRENT_USER_KEY, model.attributes)
window.location.reload() if model.get('email') isnt oldEmail
})

View file

@ -1,7 +1,7 @@
CocoClass = require 'lib/CocoClass'
{me, CURRENT_USER_KEY} = require 'lib/auth'
{backboneFailure} = require 'lib/errors'
{saveObjectToStorage} = require 'lib/storage'
storage = require 'lib/storage'
# gplus user object props to
userPropsToSave =
@ -73,7 +73,7 @@ module.exports = GPlusHandler = class GPlusHandler extends CocoClass
error: backboneFailure,
url: "/db/user?gplusID=#{gplusID}&gplusAccessToken=#{@accessToken}"
success: (model) ->
saveObjectToStorage(CURRENT_USER_KEY, model.attributes)
storage.save(CURRENT_USER_KEY, model.attributes)
window.location.reload()
})

View file

@ -1,6 +1,6 @@
{backboneFailure, genericFailure} = require 'lib/errors'
User = require 'models/User'
{saveObjectToStorage, loadObjectFromStorage} = require 'lib/storage'
storage = require 'lib/storage'
module.exports.CURRENT_USER_KEY = CURRENT_USER_KEY = 'whoami'
BEEN_HERE_BEFORE_KEY = 'beenHereBefore'
@ -10,7 +10,7 @@ module.exports.createUser = (userObject, failure=backboneFailure) ->
user.save({}, {
error: failure,
success: (model) ->
saveObjectToStorage(CURRENT_USER_KEY, model)
storage.save(CURRENT_USER_KEY, model)
window.location.reload()
})
@ -21,7 +21,7 @@ module.exports.loginUser = (userObject, failure=genericFailure) ->
password:userObject.password
},
(model) ->
saveObjectToStorage(CURRENT_USER_KEY, model)
storage.save(CURRENT_USER_KEY, model)
window.location.reload()
)
jqxhr.fail(failure)
@ -29,7 +29,7 @@ module.exports.loginUser = (userObject, failure=genericFailure) ->
module.exports.logoutUser = ->
FB?.logout?()
res = $.post('/auth/logout', {}, ->
saveObjectToStorage(CURRENT_USER_KEY, null)
storage.save(CURRENT_USER_KEY, null)
window.location.reload()
)
res.fail(genericFailure)
@ -38,7 +38,7 @@ init = ->
# Load the user from local storage, and refresh it from the server.
# Also refresh and cache the gravatar info.
storedUser = loadObjectFromStorage(CURRENT_USER_KEY)
storedUser = storage.load(CURRENT_USER_KEY)
firstTime = not storedUser
module.exports.me = window.me = new User(storedUser)
me.url = -> '/auth/whoami'
@ -50,14 +50,14 @@ init = ->
# Assign testGroupNumber to returning visitors; new ones in server/handlers/user
me.set 'testGroupNumber', Math.floor(Math.random() * 256)
me.save()
saveObjectToStorage(CURRENT_USER_KEY, me.attributes)
storage.save(CURRENT_USER_KEY, me.attributes)
me.loadGravatarProfile() if me.get('email')
me.on('sync', userSynced)
userSynced = (user) ->
Backbone.Mediator.publish('me:synced', {me:user})
saveObjectToStorage(CURRENT_USER_KEY, user)
storage.save(CURRENT_USER_KEY, user)
init()
@ -71,7 +71,7 @@ Backbone.Mediator.subscribe('level-set-volume', onSetVolume, module.exports)
trackFirstArrival = ->
# will have to filter out users who log in with existing accounts separately
# but can at least not track logouts as first arrivals using local storage
beenHereBefore = loadObjectFromStorage(BEEN_HERE_BEFORE_KEY)
beenHereBefore = storage.load(BEEN_HERE_BEFORE_KEY)
return if beenHereBefore
window.tracker?.trackEvent 'First Arrived'
saveObjectToStorage(BEEN_HERE_BEFORE_KEY, true)
storage.save(BEEN_HERE_BEFORE_KEY, true)

View file

@ -1,4 +1,4 @@
module.exports.loadObjectFromStorage = (key) ->
module.exports.load = (key) ->
s = localStorage.getItem(key)
return null unless s
try
@ -8,6 +8,8 @@ module.exports.loadObjectFromStorage = (key) ->
console.warning('error loading from storage', key)
return null
module.exports.saveObjectToStorage = (key, value) ->
module.exports.save = (key, value) ->
s = JSON.stringify(value)
localStorage.setItem(key, s)
localStorage.setItem(key, s)
module.exports.remove = (key) -> localStorage.removeItem key

View file

@ -186,6 +186,7 @@ module.exports = class Camera extends CocoClass
# Target is either just a {x, y} pos or a display object with {x, y} that might change; surface coordinates.
time = 0 if @instant
newTarget ?= {x:0, y:0}
newTarget = (@newTarget or @target) if @locked
newZoom = Math.min((Math.max @minZoom, newZoom), MAX_ZOOM)
return if @zoom is newZoom and newTarget is newTarget.x and newTarget.y is newTarget.y

View file

@ -1,5 +1,5 @@
module.exports.thangNames = thangNames =
"Soldier": [
"Soldier M": [
"William"
"Lucas"
"Marcus"
@ -45,16 +45,18 @@ module.exports.thangNames = thangNames =
"Sterling"
"Alistair"
"Remy"
"Lana"
"Stormy"
"Halle"
"Sage"
]
"Soldier F": [
"Sarah"
"Alexandra"
"Holly"
"Trinity"
"Nikita"
"Alana"
"Sage"
"Lana"
]
"Peasant": [
"Yorik"
@ -77,7 +79,7 @@ module.exports.thangNames = thangNames =
"Bernadette"
"Hershell"
]
"Archer": [
"Archer F": [
"Phoebe"
"Mira"
"Agapi"
@ -98,13 +100,15 @@ module.exports.thangNames = thangNames =
"Clare"
"Rowan"
"Omar"
"Brian"
"Cole"
"Alden"
"Cairn"
"Jensen"
]
"Ogre Munchkin": [
"Archer M": [
"Brian"
"Cole"
]
"Ogre Munchkin M": [
"Brack"
"Gort"
"Weeb"
@ -128,7 +132,10 @@ module.exports.thangNames = thangNames =
"Snortt"
"Kog"
]
"Ogre": [
"Ogre Munchkin F": [
]
"Ogre M": [
"Krogg"
"Dronck"
"Trogdor"
@ -140,6 +147,9 @@ module.exports.thangNames = thangNames =
"Nareng"
"Morthrug"
"Glonc"
]
"Ogre F": [
]
"Ogre Brawler": [
"Grul'thock"

View file

@ -114,4 +114,7 @@ class Rectangle
@deserialize: (o, world, classMap) ->
new Rectangle o.x, o.y, o.w, o.h, o.r
serializeForAether: -> @serialize()
@deserializeFromAether: (o) -> @deserialize o
module.exports = Rectangle

View file

@ -25,7 +25,7 @@ module.exports = class Thang
Thang.lastIDNums ?= {}
names = thangNames[spriteName]
order = @ordering spriteName
if names
if names and names.length
lastIDNum = Thang.lastIDNums[spriteName]
idNum = (if lastIDNum? then lastIDNum + 1 else 0)
Thang.lastIDNums[spriteName] = idNum
@ -158,6 +158,9 @@ module.exports = class Thang
t[prop] = val
t
serializeForAether: ->
{CN: @constructor.className, id: @id}
getSpriteOptions: ->
colorConfigs = @world?.getTeamColors() or {}
options = {}

View file

@ -119,4 +119,7 @@ class Vector
@deserialize: (o, world, classMap) ->
new Vector o.x, o.y, o.z
serializeForAether: -> @serialize()
@deserializeFromAether: (o) -> @deserialize o
module.exports = Vector

View file

@ -4,7 +4,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
saving: "Guardando..."
sending: "Enviando..."
cancel: "Cancelar"
# save: "Save"
save: "Guardar"
delay_1_sec: "1 segundo"
delay_3_sec: "3 segundos"
delay_5_sec: "5 segundos"
@ -31,15 +31,15 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
about: "Sobre nosotros"
contact: "Contacta"
twitter_follow: "Síguenos"
# employers: "Employers"
employers: "Empresas"
# versions:
# save_version_title: "Save New Version"
# new_major_version: "New Major Version"
# cla_prefix: "To save changes, first you must agree to our"
# cla_url: "CLA"
# cla_suffix: "."
# cla_agree: "I AGREE"
versions:
save_version_title: "Guardar nueva versión"
new_major_version: "Nueva versión principal"
cla_prefix: "Para guardar los cambios, primero debes aceptar nuestro"
cla_url: "CLA"
cla_suffix: "."
cla_agree: "De acuerdo"
login:
sign_up: "Crear una cuenta"
@ -49,10 +49,10 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
recover:
recover_account_title: "recuperar cuenta"
# send_password: "Send Recovery Password"
send_password: "Enviar recuperación de contraseña"
signup:
# create_account_title: "Create Account to Save Progress"
create_account_title: "Crea una cuenta para guardar tu progreso"
description: "Es gratis. Solo necesitamos un par de cosas y listo para comenzar!"
email_announcements: "Recibir noticias por correo electrónico"
coppa: "Soy mayor de 13 o de fuera de los Estados Unidos"
@ -70,7 +70,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
play:
choose_your_level: "Elige tu nivel"
adventurer_prefix: "Puedes elegir cualquier pantalla o charlar en "
adventurer_forum: "el foro del aventurero"
adventurer_forum: "el foro del aventurero "
adventurer_suffix: "sobre ello."
campaign_beginner: "Campaña de Principiante"
campaign_beginner_description: "... en la que aprenderás la magia de la programación."
@ -96,14 +96,14 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
diplomat_suggestion:
title: "¡Ayuda a traducir CodeCombat!"
sub_heading: "Necesitamos tus habilidades lingüisticas."
pitch_body: "Nosotros desarrollamos CodeCombat en inglés, pero ya tenemos jugadores de todo el mundo. Muchos de ellos quieren jugar en Español porque no hablan inglés, así quesi hablas ambos idiomas, inscríbete como Diplomático y ayuda a traducir la web y todos los niveles de CodeCombat al Español."
missing_translations: "Mientras terminamos la traducción al Español, verás en inglés las partes que no estén todavía disponibles."
pitch_body: "Nosotros desarrollamos CodeCombat en inglés, pero ya tenemos jugadores de todo el mundo. Muchos de ellos quieren jugar en español porque no hablan inglés, así que si hablas ambos idiomas, inscríbete como Diplomático y ayuda a traducir la web y todos los niveles de CodeCombat al español."
missing_translations: "Mientras terminamos la traducción al español, verás en inglés las partes que no estén todavía disponibles."
learn_more: "Aprende más sobre ser un Diplomático"
subscribe_as_diplomat: "Suscríbete como Diplomático"
# wizard_settings:
# title: "Wizard Settings"
# customize_avatar: "Customize Your Avatar"
wizard_settings:
title: "Ajustes del mago"
customize_avatar: "Personaliza tu Avatar"
account_settings:
title: "Ajustes de la cuenta"
@ -122,7 +122,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
new_password_verify: "Verificar"
email_subscriptions: "Suscripciones de correo electrónico"
email_announcements: "Noticias"
# email_notifications_description: "Get periodic notifications for your account."
email_notifications_description: "Recibe notificaciones periódicas para tu cuenta."
email_announcements_description: "Recibe correos electrónicos con las últimas noticias y desarrollos de CodeCombat."
contributor_emails: "Correos para colaboradores"
contribute_prefix: "¡Buscamos gente que se una a nuestro comunidad! Comprueba la "
@ -139,15 +139,15 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
# profile_for_suffix: ""
profile: "Perfil"
user_not_found: "No se encontró al usuario. ¿Comprueba la URL?"
gravatar_not_found_mine: "No podemos encontrar el prefil asociado con:"
gravatar_not_found_mine: "No podemos encontrar el perfil asociado con:"
# gravatar_not_found_email_suffix: "."
gravatar_signup_prefix: "Suscribete "
gravatar_signup_prefix: "¡Suscribete en "
gravatar_signup_suffix: " para ponerte en marcha!"
gravatar_not_found_other: "Vaya, no hay un perfil asociado a la dirección de correo electrónico de esta persona."
gravatar_contact: "Contacto"
gravatar_websites: "Paginas web"
gravatar_accounts: "Como se vé en"
gravatar_profile_link: "Prefil de Gravatar completo"
gravatar_profile_link: "Perfil de Gravatar completo"
play_level:
level_load_error: "No se pudo cargar el nivel."
@ -191,7 +191,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
tome_select_a_thang: "Selecciona a alguien para "
tome_available_spells: "Hechizos disponibles"
hud_continue: "Continuar (pulsa Shift+Space)"
# spell_saved: "Spell Saved"
spell_saved: "Hechizo guardado"
# admin:
# av_title: "Admin Views"
@ -203,29 +203,29 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
# u_title: "User List"
# lg_title: "Latest Games"
# editor:
editor:
# main_title: "CodeCombat Editors"
# main_description: "Build your own levels, campaigns, units and educational content. We provide all the tools you need!"
# article_title: "Article Editor"
main_description: "Construye tus propios niveles, campañas, unidades y contenido educativo. ¡Nosotros te ofrecemos todas las herramientas que necesitas!"
article_title: "Editor de artículos"
# article_description: "Write articles that give players overviews of programming concepts which can be used across a variety of levels and campaigns."
# thang_title: "Thang Editor"
# thang_description: "Build units, defining their default logic, graphics and audio. Currently only supports importing Flash exported vector graphics."
# level_title: "Level Editor"
level_title: "Editor de Niveles"
# level_description: "Includes the tools for scripting, uploading audio, and constructing custom logic to create all sorts of levels. Everything we use ourselves!"
# security_notice: "Many major features in these editors are not currently enabled by default. As we improve the security of these systems, they will be made generally available. If you'd like to use these features sooner, "
# contact_us: "contact us!"
# hipchat_prefix: "You can also find us in our"
# hipchat_url: "HipChat room."
contact_us: "¡Contacta con nosotros!"
hipchat_prefix: "También puedes encontrarnos en nuestra"
# hipchat_url: "sala de HipChat."
# level_some_options: "Some Options?"
# level_tab_thangs: "Thangs"
# level_tab_scripts: "Scripts"
# level_tab_settings: "Settings"
level_tab_settings: "Ajustes"
# level_tab_components: "Components"
# level_tab_systems: "Systems"
# level_tab_thangs_title: "Current Thangs"
# level_tab_thangs_conditions: "Starting Conditions"
# level_tab_thangs_add: "Add Thangs"
# level_settings_title: "Settings"
level_settings_title: "Ajustes"
# level_component_tab_title: "Current Components"
# level_component_btn_new: "Create New Component"
# level_systems_tab_title: "Current Systems"
@ -239,86 +239,86 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
# new_component_title: "Create New Component"
# new_component_field_system: "System"
# article:
# edit_btn_preview: "Preview"
# edit_article_title: "Edit Article"
article:
edit_btn_preview: "Vista preliminar"
edit_article_title: "Editar artículo"
# general:
# and: "and"
general:
and: "y"
or: "o"
# name: "Name"
name: "Nombre"
# body: "Body"
# version: "Version"
version: "Versión"
# commit_msg: "Commit Message"
# version_history_for: "Version History for: "
# results: "Results"
# description: "Description"
results: "Resultados"
description: "Descripción"
email: "Correo electrónico"
message: "Mensaje"
# about:
# who_is_codecombat: "Who is CodeCombat?"
# why_codecombat: "Why CodeCombat?"
about:
who_is_codecombat: "¿Qué es CodeCombat?"
why_codecombat: "¿Por qué CodeCombat?"
# who_description_prefix: "together started CodeCombat in 2013. We also created "
# who_description_suffix: "in 2008, growing it to the #1 web and iOS application for learning to write Chinese and Japanese characters."
# who_description_ending: "Now it's time to teach people to write code."
# who_description_ending: "Es hora de empezar a enseñar a la gente a escribir código."
# why_paragraph_1: "When making Skritter, George didn't know how to program and was constantly frustrated by his inability to implement his ideas. Afterwards, he tried learning, but the lessons were too slow. His housemate, wanting to reskill and stop teaching, tried Codecademy, but \"got bored.\" Each week another friend started Codecademy, then dropped off. We realized it was the same problem we'd solved with Skritter: people learning a skill via slow, intensive lessons when what they need is fast, extensive practice. We know how to fix that."
# why_paragraph_2: "Need to learn to code? You don't need lessons. You need to write a lot of code and have a great time doing it."
# why_paragraph_3_prefix: "That's what programming is about. It's gotta be fun. Not fun like"
# why_paragraph_3_italic: "yay a badge"
# why_paragraph_3_center: "but fun like"
# why_paragraph_3_italic_caps: "NO MOM I HAVE TO FINISH THE LEVEL!"
# why_paragraph_3_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_4: "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."
# why_ending: "And hey, it's free. "
# why_ending_url: "Start wizarding now!"
why_paragraph_3_prefix: "De eso va la programación. Tiene que ser divertido. No divertido como:"
why_paragraph_3_italic: "¡bien una insignia!,"
why_paragraph_3_center: "sino más bien como:"
why_paragraph_3_italic_caps: "¡NO MAMA, TENGO QUE TERMINAR EL NIVEL!"
why_paragraph_3_suffix: "Por eso Codecombat es multijugador, no un curso con lecciones \"gamificadas\" . No pararemos hasta que tú no puedas parar... pero esta vez, eso será buena señal."
why_paragraph_4: "Si vas a engancharte a algún juego, engánchate a este y conviértete en uno de los magos de la era tecnológica."
why_ending: "Y, oye, es gratis. "
why_ending_url: "Comienza a hacer magia ¡ya!"
# george_description: "CEO, business guy, web designer, game designer, and champion of beginning programmers everywhere."
# scott_description: "Programmer extraordinaire, software architect, kitchen wizard, and master of finances. Scott is the reasonable one."
# nick_description: "Programming wizard, eccentric motivation mage, and upside-down experimenter. Nick can do anything and chooses to build CodeCombat."
# jeremy_description: "Customer support mage, usability tester, and community organizer; you've probably already spoken with Jeremy."
# michael_description: "Programmer, sys-admin, and undergrad technical wunderkind, Michael is the person keeping our servers online."
# legal:
# page_title: "Legal"
# opensource_intro: "CodeCombat is free to play and completely open source."
# opensource_description_prefix: "Check out "
# github_url: "our GitHub"
# opensource_description_center: "and help out if you like! CodeCombat is built on dozens of open source projects, and we love them. See "
# archmage_wiki_url: "our Archmage wiki"
# opensource_description_suffix: "for a list of the software that makes this game possible."
# practices_title: "Respectful Best Practices"
# practices_description: "These are our promises to you, the player, in slightly less legalese."
# privacy_title: "Privacy"
# privacy_description: "We will not sell any of your personal information. We intend to make money through recruitment eventually, but rest assured we will not distribute your personal information to interested companies without your explicit consent."
# security_title: "Security"
# security_description: "We strive to keep your personal information safe. As an open source project, our site is freely open to anyone to review and improve our security systems."
# email_title: "Email"
# email_description_prefix: "We will not inundate you with spam. Through"
# email_settings_url: "your email settings"
# email_description_suffix: "or through links in the emails we send, you can change your preferences and easily unsubscribe at any time."
# cost_title: "Cost"
# cost_description: "Currently, CodeCombat is 100% free! One of our main goals is to keep it that way, so that as many people can play as possible, regardless of place in life. If the sky darkens, we might have to charge subscriptions or for some content, but we'd rather not. With any luck, we'll be able to sustain the company with:"
# recruitment_title: "Recruitment"
# recruitment_description_prefix: "Here on CodeCombat, you're going to become a powerful wizardnot just in the game, but also in real life."
# url_hire_programmers: "No one can hire programmers fast enough"
# recruitment_description_suffix: "so once you've sharpened your skills and if you agree, we will demo your best coding accomplishments to the thousands of employers who are drooling for the chance to hire you. They pay us a little, they pay you"
# recruitment_description_italic: "a lot"
# recruitment_description_ending: "the site remains free and everybody's happy. That's the plan."
# copyrights_title: "Copyrights and Licenses"
# contributor_title: "Contributor License Agreement"
# contributor_description_prefix: "All contributions, both on the site and on our GitHub repository, are subject to our"
# cla_url: "CLA"
# contributor_description_suffix: "to which you should agree before contributing."
legal:
page_title: "Legal"
opensource_intro: "CodeCombat es gratis y totalmente open source."
opensource_description_prefix: "Echa un vistazo a "
github_url: "nuestro GitHub"
opensource_description_center: "y ayúdanos si quieres. CodeCombat está desarrollado sobre docenas de proyectos open source, y nos encantana. Mira "
archmage_wiki_url: "nuestra wiki del Archimago"
opensource_description_suffix: "para encontrar una lista del software que hace este juego posible."
practices_title: "Prácticas respetuosas"
practices_description: "Esto es lo que te prometemos a ti, el jugador, sin usar mucha jerga legal."
privacy_title: "Privacidad"
privacy_description: "No venderemos tu información personal. Tenemos la intención de hacer dinero a través de la contratación con el tiempo, pero puedes estar seguro que no vamos a distribuir tu información personal a las empresas interesadas sin tu consentimiento expreso."
security_title: "Seguridad"
security_description: "Nos esforzamos por mantener segura tu información personal. Como proyecto de código abierto, nuestro sitio está abierto a cualquiera que quiera revisarlo y mejorar nuestros sistemas de seguridad."
email_title: "Correo electrónico"
email_description_prefix: "No te inundaremos con spam. Mediante"
email_settings_url: "tus ajustes de correo electrónico"
email_description_suffix: "o a través de los enlaces en los correos que te enviemos, puedes cambiar tus preferencias y darte de baja fácilmente en cualquier momento."
cost_title: "Precio"
cost_description: "Actualmente, ¡CodeCombat es 100% gratis! Uno de nuestros principales objetivos es mantenerlo así, de forma que el mayor número posible de gente pueda jugar, independientemente de sus posibilidades económicas. Si las cosas se tuercen, quizás tengamos que cobrar suscripciones o por algún contenido, pero preferimos no hacerlo. Con un poco de suerte, podremos mantener la empresa con: "
recruitment_title: "Contratación"
recruitment_description_prefix: "En CodeCombat, te vas a convertir en un poderoso mago no solo en el juego, también en el mundo real."
url_hire_programmers: "Nadie puede contratar programadores con la suficiente rapidez"
recruitment_description_suffix: "así que una vez que hayas afilado tus habilidades y si estás de acuerdo, mostraremos tus mejores logros en programación a los miles de empresas que están deseando tener la oportunidad de contratarte. Ellos nos pagan un poco y ellos te pagan a ti"
recruitment_description_italic: "un montón."
recruitment_description_ending: "La web permanece gratuita y todo el mundo es feliz. Ese es el plan."
copyrights_title: "Copyrights y Licencias"
contributor_title: "Acuerdo de Licencia del Colaborador"
contributor_description_prefix: "Todas las colaboraciones, tanto en la web como en nuestro repositorio de GitHub, están sujetas a nuestro"
cla_url: "CLA"
contributor_description_suffix: "con el que deberás estar de acuerdo antes de colaborar."
# code_title: "Code - MIT"
# code_description_prefix: "All code owned by CodeCombat or hosted on codecombat.com, both in the GitHub repository or in the codecombat.com database, is licensed under the"
# mit_license_url: "MIT license"
mit_license_url: "Licencia MIT"
# code_description_suffix: "This includes all code in Systems and Components that are made available by CodeCombat for the purpose of creating levels."
# art_title: "Art/Music - Creative Commons "
# art_description_prefix: "All common content is available under the"
art_title: "Arte/Música - Creative Commons "
art_description_prefix: "Todo el contenido común está disponible bajo la"
# cc_license_url: "Creative Commons Attribution 4.0 International License"
# art_description_suffix: "Common content is anything made generally available by CodeCombat for the purpose of creating Levels. This includes:"
# art_music: "Music"
# art_sound: "Sound"
art_music: "Música"
art_sound: "Sonido"
# art_artwork: "Artwork"
# art_sprites: "Sprites"
# art_other: "Any and all other non-code creative works that are made available when creating Levels."
@ -339,8 +339,8 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
# nutshell_description: "Any resources we provide in the Level Editor are free to use as you like for creating Levels. But we reserve the right to restrict distribution of the Levels themselves (that are created on codecombat.com) so that they may be charged for in the future, if that's what ends up happening."
# canonical: "The English version of this document is the definitive, canonical version. If there are any discrepencies between translations, the English document takes precedence."
# contribute:
# page_title: "Contributing"
contribute:
page_title: "Colaborar"
# character_classes_title: "Character Classes"
# introduction_desc_intro: "We have high hopes for CodeCombat."
# introduction_desc_pref: "We want to be where programmers of all stripes come to learn and play together, introduce others to the wonderful world of coding, and reflect the best parts of the community. We can't and don't want to do that alone; what makes projects like GitHub, Stack Overflow and Linux great are the people who use them and build on them. To that end, "
@ -424,8 +424,8 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
# translating_diplomats: "Our Translating Diplomats:"
# helpful_ambassadors: "Our Helpful Ambassadors:"
# classes:
# archmage_title: "Archmage"
classes:
archmage_title: "Archimago"
# archmage_title_description: "(Coder)"
# artisan_title: "Artisan"
# artisan_title_description: "(Level Builder)"

View file

@ -205,46 +205,46 @@ module.exports = nativeDescription: "magyar", englishDescription: "Hungarian", t
editor:
main_title: "CodeCombat szerkesztők"
main_description: "KLészíts saját pályákat, hadjáratokat, egységeket és oktatési célú tartalmakat. Mi megadunk hozzá minden eszközt amire csak szükséged lehet!"
# article_title: "Article Editor"
# article_description: "Write articles that give players overviews of programming concepts which can be used across a variety of levels and campaigns."
# thang_title: "Thang Editor"
# thang_description: "Build units, defining their default logic, graphics and audio. Currently only supports importing Flash exported vector graphics."
# level_title: "Level Editor"
# level_description: "Includes the tools for scripting, uploading audio, and constructing custom logic to create all sorts of levels. Everything we use ourselves!"
# security_notice: "Many major features in these editors are not currently enabled by default. As we improve the security of these systems, they will be made generally available. If you'd like to use these features sooner, "
# contact_us: "contact us!"
# hipchat_prefix: "You can also find us in our"
# hipchat_url: "HipChat room."
# level_some_options: "Some Options?"
# level_tab_thangs: "Thangs"
# level_tab_scripts: "Scripts"
# level_tab_settings: "Settings"
# level_tab_components: "Components"
# level_tab_systems: "Systems"
# level_tab_thangs_title: "Current Thangs"
# level_tab_thangs_conditions: "Starting Conditions"
# level_tab_thangs_add: "Add Thangs"
# level_settings_title: "Settings"
# level_component_tab_title: "Current Components"
# level_component_btn_new: "Create New Component"
# level_systems_tab_title: "Current Systems"
# level_systems_btn_new: "Create New System"
# level_systems_btn_add: "Add System"
# level_components_title: "Back to All Thangs"
# level_components_type: "Type"
# level_component_edit_title: "Edit Component"
# level_system_edit_title: "Edit System"
# create_system_title: "Create New System"
# new_component_title: "Create New Component"
# new_component_field_system: "System"
main_description: "Készíts saját pályákat, hadjáratokat, egységeket és oktatési célú tartalmakat. Mi megadunk hozzá minden eszközt amire csak szükséged lehet!"
article_title: "Cikk szerkesztő"
article_description: "Írhatsz cikkeket, hogy átfogó képet adhass olyan programozási szemléletekről, melyeket a különböző pályákon és küldetések során felhasználhatnak."
thang_title: "Eszköz szerkesztő"
thang_description: "Építs egységeket, határozd meg az működésüket, kinézetüket és hangjukat. Jelenleg csak a Flash-ből exportált vektorgrafika támogatott."
level_title: "Pálya szerkesztő"
level_description: "Mindent magába foglal, ami kódolás, hangok feltöltése, és a pályák teljesen egyedi felépítése. Minden, amit mi használunk!"
security_notice: "Számos főbb funkció ezekben a szerkesztőkben még nincs engedélyezve alapesetben. Amint a rendszer biztonságát növelni tudjuk, elérhetővé teszzük ezeket. Ha a későbbiekben használni szeretnéf ezeket a funkciókat, "
contact_us: "lépj kapcsolatba velünk!"
hipchat_prefix: "Megtalálhatsz bennünket a "
hipchat_url: "HipChat szobában."
level_some_options: "Néhány beállítás?"
level_tab_thangs: "Eszközök"
level_tab_scripts: "Kódok"
level_tab_settings: "Beállítások"
level_tab_components: "Komponensek"
level_tab_systems: "Rendszerek"
level_tab_thangs_title: "Jelenlegi eszközök"
level_tab_thangs_conditions: "Kezdő feltételek"
level_tab_thangs_add: "Eszköz hozzáadása"
level_settings_title: "Beállítások"
level_component_tab_title: "Jelenlegi komponensek"
level_component_btn_new: "Új komponens készítése"
level_systems_tab_title: "Jelenlegi rendszerek"
level_systems_btn_new: "Új rendszer készítése"
level_systems_btn_add: "Rendszer hozzáadása"
level_components_title: "Vissza az összes eszközhöz"
level_components_type: "Típus"
level_component_edit_title: "Komponens szerkesztése"
level_system_edit_title: "Rendszer szerkesztése"
create_system_title: "Új rendszer készítése"
new_component_title: "Új komponens készítése"
new_component_field_system: "Rendszer"
# article:
# edit_btn_preview: "Preview"
# edit_article_title: "Edit Article"
article:
edit_btn_preview: "Előnézet"
edit_article_title: "Cikk szerkesztése"
general:
# and: "and"
and: "és"
or: "vagy "
name: "Név"
# body: "Body"

View file

@ -3,3 +3,4 @@ CocoModel = require('./CocoModel')
module.exports = class Article extends CocoModel
@className: "Article"
urlRoot: "/db/article"
saveBackups: true

View file

@ -1,3 +1,5 @@
storage = require 'lib/storage'
class CocoSchema extends Backbone.Model
constructor: (path, args...) ->
super(args...)
@ -9,6 +11,7 @@ class CocoModel extends Backbone.Model
idAttribute: "_id"
loaded: false
loading: false
saveBackups: false
@schema: null
initialize: ->
@ -20,15 +23,32 @@ class CocoModel extends Backbone.Model
@addSchemaDefaults()
else
@loadSchema()
@once 'sync', @onLoaded
@once 'sync', @onLoaded, @
@saveBackup = _.debounce(@saveBackup, 500)
type: ->
@constructor.className
onLoaded: =>
onLoaded: ->
@loaded = true
@loading = false
@markToRevert()
if @saveBackups
existing = storage.load @id
if existing
@set(existing, {silent:true})
CocoModel.backedUp[@id] = @
set: ->
res = super(arguments...)
@saveBackup() if @saveBackups and @loaded
res
saveBackup: ->
storage.save(@id, @attributes)
CocoModel.backedUp[@id] = @
@backedUp = {}
loadSchema: ->
unless @constructor.schema
@ -38,7 +58,6 @@ class CocoModel extends Backbone.Model
@constructor.schema.on 'sync', =>
@constructor.schema.loaded = true
@addSchemaDefaults()
@markToRevert()
@trigger 'schema-loaded'
@hasSchema: -> return @schema?.loaded
@ -57,6 +76,7 @@ class CocoModel extends Backbone.Model
@trigger "save:success", @
success(@, resp) if success
@markToRevert()
@clearBackup()
@trigger "save", @
return super attrs, options
@ -69,6 +89,10 @@ class CocoModel extends Backbone.Model
revert: ->
@set(@_revertAttributes, {silent: true}) if @_revertAttributes
@clearBackup()
clearBackup: ->
storage.remove @id
hasLocalChanges: ->
not _.isEqual @attributes, @_revertAttributes

View file

@ -6,6 +6,7 @@ class SuperModel
populateModel: (model) ->
@mustPopulate = model
model.saveBackups = @shouldSaveBackups(model)
model.fetch() unless model.loaded or model.loading
model.on('sync', @modelLoaded) unless model.loaded
model.once('error', @modelErrored) unless model.loaded
@ -13,7 +14,9 @@ class SuperModel
@models[url] = model unless @models[url]?
@modelLoaded(model) if model.loaded
shouldPopulate: (url) -> return true # replace or overwrite
# replace or overwrite
shouldPopulate: (url) -> return true
shouldSaveBackups: (model) -> return false
modelErrored: (model) =>
@trigger 'error'
@ -25,6 +28,7 @@ class SuperModel
refs = [] unless @mustPopulate is model or @shouldPopulate(model)
# console.log 'Loaded', model.get('name')
for ref, i in refs
ref.saveBackups = @shouldSaveBackups(ref)
refURL = ref.url()
continue if @models[refURL]
@models[refURL] = ref

View file

@ -1,5 +1,5 @@
#editor-thang-type-edit-view
#save-button
#save-button, #revert-button
float: right
margin-right: 20px

View file

@ -0,0 +1,3 @@
#revert-modal
table
width: 100%

View file

@ -31,13 +31,13 @@
#cast-button-view
display: none
position: absolute
width: 35%
.cast-button-group
position: absolute
top: 55px
left: 20px
z-index: 2
@include opacity(77)
width: 100%
.button-progress-overlay
position: absolute
@ -75,7 +75,8 @@
padding: 3px 10px
.cast-button
width: 90px
width: 100%
height: 29px
.autocast-delays
min-width: 0

View file

@ -3,7 +3,7 @@
.problem-alert
z-index: 10
position: absolute
bottom: -70px
bottom: -110px
left: 10px
right: 10px
background: transparent url(/images/level/code_editor_error_background.png) no-repeat

View file

@ -21,19 +21,26 @@
.save-status
display: none
position: relative
padding-top: 2px
padding-left: 10px
position: absolute
bottom: 2%
left: 1%
.firepad
width: 100%
height: 100%
@include box-sizing(border-box)
// When Firepad is active, it wraps .ace_editor in .firepad.
width: 98%
height: 83%
height: -webkit-calc(100% - 60px - 40px)
height: calc(100% - 60px - 40px)
.ace_editor
width: 100%
height: 100%
.ace_editor
@include box-sizing(border-box)
margin-top: 40px
width: 100%
// When Firepad isn't active, .ace_editor needs the width/height set itself.
width: 98%
height: 83%
height: -webkit-calc(100% - 60px - 40px)
height: calc(100% - 60px - 40px)
@ -61,9 +68,9 @@
.executing, .executed, .problem-marker-info, .problem-marker-warning, .problem-marker-error
position: absolute
.executing
background-color: rgba(216, 255, 255, 0.55)
background-color: rgba(216, 255, 255, 0.85)
.executed
background-color: rgba(216, 255, 255, 0.25)
background-color: rgba(245, 255, 6, 0.18)
.problem-marker-info
background-color: rgba(96, 63, 84, 0.25)
.problem-marker-warning
@ -79,7 +86,7 @@
.ace_marker-layer
.ace_bracket
// Override faint gray
border-color: #8FF
border-color: #BFF
.ace_identifier
background-color: rgba(255, 128, 128, 0.15)
border-bottom: 1px dotted rgba(255, 128, 128, 0.45)

View file

@ -1,54 +1,82 @@
@import "../../../bootstrap/mixins"
.spell-toolbar-view
position: absolute
z-index: 2
top: 2px
left: 5px
position: relative
box-sizing: border-box
padding-left: 150px
height: 36px
width: 95%
width: -webkit-calc(95% - 5px)
width: calc(95% - 5px)
background-color: rgba(100, 45, 210, 0.05)
margin: 4px 1%
height: 45px
width: 97%
//background-color: rgba(100, 45, 210, 0.15)
.spell-progress
position: relative
height: 100%
width: 50%
display: inline-block
.flow
&:hover .spell-progress
opacity: 1
.progress
.spell-progress
position: absolute
left: 0px
top: 8px
bottom: 0px
width: 100%
height: 100%
width: 40%
left: 45%
display: inline-block
cursor: pointer
overflow: visible
.bar
@include transition(width .0s linear)
position: relative
box-sizing: border-box
opacity: 0.25
.progress
position: absolute
left: 0px
top: 12.5px
bottom: 0px
width: 100%
height: 4px
overflow: visible
pointer-events: none
background-color: #67A4C8
width: 50%
.scrubber-handle
position: absolute
.bar
@include transition(width .0s linear)
position: relative
pointer-events: none
right: -16px
top: -7px
background: transparent url(/images/level/playback_thumb.png)
width: 32px
height: 32px
background: linear-gradient(#2c3e5f, #2c3e5f 16%, #3a537f 16%, #3a537f 83%, #2c3e5f 84%, #2c3e5f)
width: 50%
pointer-events: none
.scrubber-handle
position: absolute
pointer-events: none
right: -5px
top: -12.5px
background: linear-gradient(#2c3e5f, #2c3e5f 16%, #3a537f 16%, #3a537f 83%, #2c3e5f 84%, #2c3e5f)
width: 14px
height: 29px
border-radius: 3px
box-sizing: border-box
border: 1px solid black
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.5)
&:hover .steppers
opacity: 1
.btn-group
// I don't know, I can figure this out for real later
margin: -26px 0 0 18px
.metrics
display: inline-block
margin: -30px 0 0 10px
vertical-align: middle
.steppers
position: absolute
z-index: 2
width: 10%
right: 2%
box-sizing: border-box
opacity: 0.25
button
height: 29px
.metrics
display: none
top: 30px
position: absolute
z-index: 10
pointer-events: none
padding: 10px
background: transparent url(/images/level/popover_background.png)
background-size: 100% 100%
font-variant: small-caps
text-overflow: ellipsis
font-size: 13px
white-space: nowrap

View file

@ -8,7 +8,7 @@ block content
p(data-i18n="account_settings.not_logged_in") Log in or create an account to change your settings.
else
button.btn#save-button.disabled.hide(data-i18n="account_settings.autosave") Changes Save Automatically
button.btn#save-button.disabled.hide(data-i18n="account_settings.saveBackups") Changes Save Automatically
ul.nav.nav-tabs#settings-tabs
li

View file

@ -1,6 +1,7 @@
extends /templates/base
block content
button(data-toggle="coco-modal", data-target="modal/revert", data-i18n="revert.revert").btn.btn-primary#revert-button Revert
button(data-i18n="article.edit_btn_preview").btn.btn-primary#preview-button Preview
button(data-toggle="coco-modal", data-target="modal/save_version", data-i18n="common.save").btn.btn-primary#save-button Save

View file

@ -10,6 +10,7 @@ block outer_content
span.level-title #{level.attributes.name}
.level-control-buttons
button(data-toggle="coco-modal", data-target="modal/revert", data-i18n="revert.revert").btn.btn-primary#revert-button Revert
button(data-i18n="common.save").btn.btn-primary#commit-level-start-button Save
button(data-i18n="common.fork").btn.btn-primary#fork-level-start-button Fork
.btn-group.play-button-group

View file

@ -6,6 +6,7 @@ block content
button.btn.btn-primary#save-button(data-toggle="coco-modal", data-target="modal/save_version")
| Save
button.btn.btn-primary#revert-button(data-toggle="coco-modal", data-target="modal/revert", data-i18n="revert.revert") Revert
h3 Edit Thang Type: "#{thangType.attributes.name}"

View file

@ -0,0 +1,13 @@
extends /templates/modal/modal_base
block modal-header-content
h3(data-i18n="revert.revert_models") Revert Models
block modal-body-content
table.table.table-striped#changed-models
for model in models
tr
td
| #{model.type()}: #{model.get('name')}
td
button(value=model.id) Revert

View file

@ -1,7 +1,7 @@
div.btn-group.cast-button-group
.button-progress-overlay
button.btn.btn-inverse.banner.cast-button(title=castShortcutVerbose + ": Cast current spell", data-i18n="play_level.tome_cast_button_cast") Spell Cast
button.btn.btn-inverse.banner.cast-options-button.dropdown-toggle(data-toggle="dropdown")
button.btn.btn-inverse.btn-large.banner.cast-button(title=castShortcutVerbose + ": Cast current spell", data-i18n="play_level.tome_cast_button_cast") Spell Cast
button.btn.btn-inverse.btn-large.banner.cast-options-button.dropdown-toggle(data-toggle="dropdown")
i.icon-cog.icon-white
ul.dropdown-menu.autocast-delays

View file

@ -1,22 +1,25 @@
.spell-progress
.progress
.bar
.scrubber-handle
.flow
.btn-group
button.btn.btn-mini.btn-inverse.banner.step-backward(title="Ctrl/Cmd + Alt + [: Step Backward")
i.icon-arrow-left.icon-white
button.btn.btn-mini.btn-inverse.banner.step-forward(title="Ctrl/Cmd + Alt + ]: Step Forward")
i.icon-arrow-right.icon-white
.metrics
code.statements-metric
span.metric.statement-index
| /
span.metric.statements-executed
span.metric.statements-executed-total
|
code.calls-metric
span.metric.call-index
| /
span.metric.calls-executed
.spell-progress
.progress
.bar
.scrubber-handle
.btn-group.steppers
button.btn.btn-mini.btn-inverse.banner.step-backward(title="Ctrl/Cmd + Alt + [: Step Backward")
i.icon-arrow-left.icon-white
button.btn.btn-mini.btn-inverse.banner.step-forward(title="Ctrl/Cmd + Alt + ]: Step Forward")
i.icon-arrow-right.icon-white
.metrics
.statements-metric
| Statement
span.metric.statement-index
| /
span.metric.statements-executed
span.metric.statements-executed-total
.calls-metric
| Call
span.metric.call-index
| /
span.metric.calls-executed

View file

@ -16,6 +16,7 @@ module.exports = class ArticleEditView extends View
constructor: (options, @articleID) ->
super options
@article = new Article(_id: @articleID)
@article.saveBackups = true
@article.fetch()
@article.once('sync', @onArticleSync)
@article.on('schema-loaded', @buildTreema)
@ -43,6 +44,8 @@ module.exports = class ArticleEditView extends View
@treema.build()
pushChangesToPreview: =>
for key, value of @treema.data
@article.set(key, value)
return unless @treema and @preview
m = marked(@treema.data.body)
b = $(@preview.document.body)

View file

@ -31,8 +31,16 @@ module.exports = class ComponentsTabView extends View
haveThisComponent.push thang.id if haveThisComponent.length < 100 # for performance when adding many Thangs
return if _.isEqual presentComponents, @presentComponents
@presentComponents = presentComponents
treemaData = _.sortBy ({original: key.split('.')[0], majorVersion: parseInt(key.split('.')[1], 10), thangs: value, count: value.length} for key, value of @presentComponents), "count"
treemaData.reverse()
componentModels = @supermodel.getModels LevelComponent
componentModelMap = {}
componentModelMap[comp.get('original')] = comp for comp in componentModels
components = ({original: key.split('.')[0], majorVersion: parseInt(key.split('.')[1], 10), thangs: value, count: value.length} for key, value of @presentComponents)
treemaData = _.sortBy components, (comp) ->
comp = componentModelMap[comp.original]
res = [comp.get('system'), comp.get('name')]
return res
treemaOptions =
supermodel: @supermodel
schema: {type: 'array', items: {type: 'object', format: 'level-component'}}

View file

@ -36,6 +36,9 @@ module.exports = class EditorLevelView extends View
return false if @levelsLoaded > 1
return true
@supermodel.shouldSaveBackups = (model) ->
model.constructor.className in ['Level', 'LevelComponent', 'LevelSystem']
@level = new Level _id: @levelID
@level.once 'sync', @onLevelLoaded
@supermodel.populateModel @level

View file

@ -30,6 +30,7 @@ module.exports = class SystemsTabView extends View
for system in @buildDefaultSystems()
url = "/db/level.system/#{system.original}/version/#{system.majorVersion}"
ls = new LevelSystem()
ls.saveBackups = true
do (url) -> ls.url = -> url
continue if @supermodel.getModelByURL ls.url
ls.fetch()
@ -57,7 +58,12 @@ module.exports = class SystemsTabView extends View
unless systems.length
systems = @buildDefaultSystems()
insertedDefaults = true
systems = _.sortBy systems, "name"
systemModels = @supermodel.getModels LevelSystem
systemModelMap = {}
systemModelMap[sys.get('original')] = sys.get('name') for sys in systemModels
systems = _.sortBy systems, (sys) -> systemModelMap[sys.original]
treemaOptions =
# TODO: somehow get rid of the + button, or repurpose it to open the LevelSystemAddView instead
supermodel: @supermodel

View file

@ -39,6 +39,7 @@ module.exports = class ThangTypeEditView extends View
super options
@mockThang = _.cloneDeep(@mockThang)
@thangType = new ThangType(_id: @thangTypeID)
@thangType.saveBackups = true
@thangType.fetch()
@thangType.once('sync', @onThangTypeSync)
@refreshAnimation = _.debounce @refreshAnimation, 500

View file

@ -0,0 +1,23 @@
ModalView = require 'views/kinds/ModalView'
template = require 'templates/modal/revert'
CocoModel = require 'models/CocoModel'
module.exports = class RevertModal extends ModalView
id: 'revert-modal'
template: template
events:
'click #changed-models button': 'onRevertModel'
onRevertModel: (e) ->
id = $(e.target).val()
CocoModel.backedUp[id].revert()
$(e.target).closest('tr').remove()
getRenderData: ->
c = super()
models = _.values CocoModel.backedUp
models = (m for m in models when m.hasLocalChanges())
c.models = models
c

View file

@ -32,6 +32,9 @@ module.exports = class CastButtonView extends View
delay ?= 5000
@setAutocastDelay delay
attachTo: (spellView) ->
@$el.detach().prependTo(spellView.toolbarView.$el).show()
hookUpButtons: ->
# hook up cast button callbacks
@castButton = $('.cast-button', @$el)

View file

@ -56,7 +56,6 @@ module.exports = class Spell
createAether: (thang) ->
aetherOptions =
thisValue: thang.createUserContext()
problems:
jshint_W040: {level: "ignore"}
aether_MissingThis: {level: (if thang.requiresThis then 'error' else 'warning')}
@ -68,7 +67,7 @@ module.exports = class Spell
#callIndex: 0
#timelessVariables: ['i']
#statementIndex: 9001
if not (me.team in @permissions.readwrite)# or @name is 'chooseAction' or thang.id is 'Thoktar' # Gridmancer can't handle it
if not (me.team in @permissions.readwrite) or window.currentView?.sessionID is "52bfb88099264e565d001349" # temp fix for debugger explosion bug
#console.log "Turning off includeFlow for", @spellKey
aetherOptions.includeFlow = false
aether = new Aether aetherOptions

View file

@ -1,51 +1,76 @@
View = require 'views/kinds/CocoView'
template = require 'templates/play/level/tome/spell_debug'
Range = ace.require("ace/range").Range
TokenIterator = ace.require("ace/token_iterator").TokenIterator
serializedClasses =
Thang: require "lib/world/thang"
Vector: require "lib/world/vector"
Rectangle: require "lib/world/rectangle"
module.exports = class DebugView extends View
className: 'spell-debug-view'
template: template
subscriptions: {}
subscriptions:
'god:new-world-created': 'onNewWorld'
events: {}
constructor: (options) ->
super options
@ace = options.ace
@thang = options.thang
@variableStates = {}
afterRender: ->
super()
@ace.on "mousemove", @onMouseMove
#@ace.on "click", onClick # same ACE API as mousemove
setVariableStates: (@variableStates) ->
@update()
onMouseMove: (e) =>
pos = e.getDocumentPosition()
column = pos.column
until column < 0
if token = e.editor.session.getTokenAt pos.row, column
break if token.type is 'identifier'
column = token.start - 1
else
--column
if token?.type is 'identifier' and token.value of @variableStates
@variable = token.value
endOfDoc = pos.row is @ace.getSession().getDocument().getLength() - 1
it = new TokenIterator e.editor.session, pos.row, pos.column
isIdentifier = (t) -> t and (t.type is 'identifier' or t.value is 'this')
while it.getCurrentTokenRow() is pos.row and not isIdentifier(token = it.getCurrentToken())
it.stepBackward()
break unless token
break if endOfDoc # Don't iterate backward on last line, since we might be way below.
if isIdentifier token
# This could be a property access, like "enemy.target.pos" or "this.spawnedRectangles".
# We have to realize this and dig into the nesting of the objects.
start = it.getCurrentTokenColumn()
[chain, start, end] = [[token.value], start, start + token.value.length]
while it.getCurrentTokenRow() is pos.row
it.stepBackward()
break unless it.getCurrentToken()?.value is "."
it.stepBackward()
token = null # If we're doing a complex access like this.getEnemies().length, then length isn't a valid var.
break unless isIdentifier(prev = it.getCurrentToken())
token = prev
start = it.getCurrentTokenColumn()
chain.unshift token.value
if token and (token.value of @variableStates or token.value is "this")
@variableChain = chain
@pos = {left: e.domEvent.offsetX + 50, top: e.domEvent.offsetY + 50}
@markerRange = new Range pos.row, token.start, pos.row, token.start + token.value.length
@markerRange = new Range pos.row, start, pos.row, end
else
@variable = @markerRange = null
@variableChain = @markerRange = null
@update()
onMouseOut: (e) =>
@variable = @markerRange = null
@variableChain = @markerRange = null
@update()
onNewWorld: (e) ->
@thang = @options.thang = e.world.thangMap[@thang.id] if @thang
update: ->
if @variable
value = @variableStates[@variable]
@$el.find("code").text "#{@variable}: #{value}"
if @variableChain
{key, value} = @deserializeVariableChain @variableChain
@$el.find("code").text "#{key}: #{value}"
@$el.show().css(@pos)
else
@$el.hide()
@ -58,6 +83,28 @@ module.exports = class DebugView extends View
if @markerRange
@marker = @ace.getSession().addMarker @markerRange, "ace_bracket", "text"
deserializeVariableChain: (chain) ->
keys = []
for prop, i in chain
if prop is "this"
value = @thang
else
value = (if i is 0 then @variableStates else value)[prop]
keys.push prop
break unless value
if theClass = serializedClasses[value.CN]
if value.CN is "Thang"
thang = @thang.world.thangMap[value.id]
value = thang or "<Thang #{value.id} (non-existent)>"
else
value = theClass.deserializeFromAether(value)
if value and not _.isString value
if value.constructor?.className is "Thang"
value = "<#{value.spriteName} - #{value.id}, #{if value.pos then value.pos.toString() else 'non-physical'}>"
else
value = value.toString()
key: keys.join("."), value: value
destroy: ->
super()
@ace?.removeEventListener "mousemove", @onMouseMove

View file

@ -4,14 +4,15 @@ template = require 'templates/play/level/tome/spell_toolbar'
module.exports = class SpellToolbarView extends View
className: 'spell-toolbar-view'
template: template
progressHoverDelay: 500
subscriptions:
'spell-step-backward': 'onStepBackward'
'spell-step-forward': 'onStepForward'
events:
'mousemove .progress': 'onProgressHover'
'mouseout .progress': 'onProgressMouseOut'
'mousemove .spell-progress': 'onProgressHover'
'mouseout .spell-progress': 'onProgressMouseOut'
'click .step-backward': 'onStepBackward'
'click .step-forward': 'onStepForward'
@ -22,20 +23,25 @@ module.exports = class SpellToolbarView extends View
afterRender: ->
super()
toggleFlow: (to) ->
@$el.find(".flow").toggle to
setStatementIndex: (statementIndex) ->
return unless total = @callState?.statementsExecuted
@statementIndex = Math.min(total - 1, Math.max(0, statementIndex))
@statementRatio = @statementIndex / (total - 1)
@statementTime = @callState.statements[@statementIndex]?.userInfo.time ? 0
@$el.find('.bar').css('width', 100 * @statementRatio + '%')
Backbone.Mediator.publish 'tome:spell-statement-index-updated', statementIndex: @statementIndex, ace: @ace
@$el.find('.step-backward').prop('disabled', @statementIndex is 0)
@$el.find('.step-forward').prop('disabled', @statementIndex is total - 1)
@updateMetrics()
_.defer =>
Backbone.Mediator.publish 'tome:spell-statement-index-updated', statementIndex: @statementIndex, ace: @ace
updateMetrics: ->
statementsExecuted = @callState.statementsExecuted
$metrics = @$el.find('.metrics')
return $metrics.hide() if @suppressMetricsUpdates or not (statementsExecuted or @metrics.statementsExecuted)
if @metrics.callsExecuted > 1
$metrics.find('.call-index').text @callIndex + 1
$metrics.find('.calls-executed').text @metrics.callsExecuted
@ -54,25 +60,40 @@ module.exports = class SpellToolbarView extends View
$metrics.find('.statements-metric').show().attr('title', "Statement #{@statementIndex + 1} of #{statementsExecuted} this call#{titleSuffix}")
else
$metrics.find('.statements-metric').hide()
left = @$el.find('.scrubber-handle').position().left + @$el.find('.spell-progress').position().left
$metrics.finish().show().css({left: left - $metrics.width() / 2}).delay(2000).fadeOut('fast')
setStatementRatio: (ratio) ->
return unless total = @callState?.statementsExecuted
@setStatementIndex Math.floor ratio * total
statementIndex = Math.floor ratio * total
@setStatementIndex statementIndex unless statementIndex is @statementIndex
onProgressHover: (e) ->
return @onProgressHoverLong(e) if @maintainIndexHover
@lastHoverEvent = e
@hoverTimeout = _.delay @onProgressHoverLong, @progressHoverDelay unless @hoverTimeout
onProgressHoverLong: (e) =>
e ?= @lastHoverEvent
@hoverTimeout = null
@setStatementRatio e.offsetX / @$el.find('.progress').width()
@updateTime()
@maintainIndexHover = true
@updateScroll()
onProgressMouseOut: (e) ->
@maintainIndexHover = false
if @hoverTimeout
clearTimeout @hoverTimeout
@hoverTimeout = null
onStepBackward: (e) -> @step -1
onStepForward: (e) -> @step 1
step: (delta) ->
lastTime = @statementTime
@setStatementIndex @statementIndex + delta
@updateTime() if @statementIndex isnt lastTime
@updateTime() if @statementTime isnt lastTime
@updateScroll()
updateTime: ->
@maintainIndexScrub = true
@ -80,15 +101,18 @@ module.exports = class SpellToolbarView extends View
@maintainIndexScrubTimeout = _.delay (=> @maintainIndexScrub = false), 500
Backbone.Mediator.publish 'level-set-time', time: @statementTime, scrubDuration: 500
updateScroll: ->
return unless statementStart = @callState?.statements?[@statementIndex]?.range[0]
text = @ace.getValue()
currentLine = text.substr(0, statementStart).split('\n').length - 1
@ace.scrollToLine currentLine, true, true
setCallState: (callState, statementIndex, @callIndex, @metrics) ->
return if callState is @callState and statementIndex is @statementIndex
return unless @callState = callState
@suppressMetricsUpdates = true
if not @maintainIndexHover and not @maintainIndexScrub and statementIndex? and callState.statements[statementIndex]?.userInfo.time isnt @statementTime
@setStatementIndex statementIndex
else
@setStatementRatio @statementRatio
# Not sure yet whether it's better to maintain @statementIndex or @statementRatio
#else if @statementRatio is 1 or not @statementIndex?
# @setStatementRatio 1
#else
# @setStatementIndex @statementIndex
@suppressMetricsUpdates = false

View file

@ -51,7 +51,6 @@ module.exports = class SpellView extends View
else
# needs to happen after the code generating this view is complete
setTimeout @onLoaded, 1
@createDebugView()
createACE: ->
# Test themes and settings here: http://ace.ajax.org/build/kitchen-sink.html
@ -69,6 +68,7 @@ module.exports = class SpellView extends View
@ace.setShowPrintMargin false
@ace.setShowInvisibles false
@ace.setBehavioursEnabled false
@ace.setAnimatedScroll true
@toggleControls null, @writable
@aceSession.selection.on 'changeCursor', @onCursorActivity
$(@ace.container).find('.ace_gutter').on 'click', '.ace_error, .ace_warning, .ace_info', @onAnnotationClick
@ -154,14 +154,15 @@ module.exports = class SpellView extends View
@spell.loaded = true
Backbone.Mediator.publish 'tome:spell-loaded', spell: @spell
@eventsSuppressed = false # Now that the initial change is in, we can start running any changed code
@createToolbarView()
createDebugView: ->
@debugView = new SpellDebugView ace: @ace
@debugView = new SpellDebugView ace: @ace, thang: @thang
@$el.append @debugView.render().$el.hide()
createToolbarView: ->
@toolbarView = new SpellToolbarView ace: @ace
@$el.prepend @toolbarView.render().$el
@$el.append @toolbarView.render().$el
onMouseOut: (e) ->
@debugView.onMouseOut e
@ -174,6 +175,8 @@ module.exports = class SpellView extends View
return if thang.id is @thang?.id
@thang = thang
@spellThang = @spell.thangs[@thang.id]
@createDebugView() unless @debugView
@debugView.thang = @thang
@updateAether false, true
@highlightCurrentLine()
@ -343,6 +346,7 @@ module.exports = class SpellView extends View
@spellHasChanged = true
onSessionWillSave: (e) ->
return unless @spellHasChanged
setTimeout(=>
unless @spellHasChanged
@$el.find('.save-status').finish().show().fadeOut(2000)
@ -432,35 +436,32 @@ module.exports = class SpellView extends View
@debugView.setVariableStates {}
@aceSession.removeGutterDecoration row, 'executing' for row in [0 ... @aceSession.getLength()]
$(@ace.container).find('.ace_gutter-cell.executing').removeClass('executing')
unless executed.length
@toolbarView?.$el.hide()
if not executed.length or (@spell.name is "plan" and @spellThang.castAether.metrics.statementsExecuted < 20)
@toolbarView?.toggleFlow false
return
unless @toolbarView or (@spell.name is "plan" and @spellThang.castAether.metrics.statementsExecuted < 20)
@createToolbarView()
lastExecuted = _.last executed
@toolbarView?.$el.show()
@toolbarView?.toggleFlow true
statementIndex = Math.max 0, lastExecuted.length - 1
@toolbarView?.setCallState states[currentCallIndex], statementIndex, currentCallIndex, @spellThang.castAether.metrics
marked = {}
lastExecuted = lastExecuted[0 .. @toolbarView.statementIndex] if @toolbarView?.statementIndex?
for state, i in lastExecuted
#clazz = if state.executing then 'executing' else 'executed' # doesn't work
[start, end] = [offsetToPos(state.range[0]), offsetToPos(state.range[1])]
clazz = if i is lastExecuted.length - 1 then 'executing' else 'executed'
if clazz is 'executed'
key = state.range[0] + '_' + state.range[1]
continue if marked[key] > 2 # don't allow more than three of the same marker
marked[key] ?= 0
++marked[key]
continue if marked[start.row]
marked[start.row] = true
markerType = "fullLine"
else
@debugView.setVariableStates state.variables
#console.log "at", state.userInfo.time, "vars are now:", state.variables
[start, end] = [offsetToPos(state.range[0]), offsetToPos(state.range[1])]
markerType = "text"
markerRange = new Range(start.row, start.column, end.row, end.column)
markerRange.start = @aceDoc.createAnchor markerRange.start
markerRange.end = @aceDoc.createAnchor markerRange.end
markerRange.id = @aceSession.addMarker markerRange, clazz, "text"
markerRange.id = @aceSession.addMarker markerRange, clazz, markerType
@markerRanges.push markerRange
@aceSession.addGutterDecoration start.row, clazz if clazz is 'executing'
null
onAnnotationClick: ->
alertBox = $("<div class='alert alert-info fade in'>#{msg}</div>")

View file

@ -138,7 +138,7 @@ module.exports = class TomeView extends View
@$el.find('#' + @spellTabView.id).after(@spellTabView.el).remove()
@spellView.setThang thang
@spellTabView.setThang thang
@castButton.$el.show()
@castButton.attachTo @spellView
@thangList.$el.hide()
@spellPaletteView = @insertSubView new SpellPaletteView thang: thang
@spellPaletteView.toggleControls {}, @spellView.controlsEnabled # TODO: know when palette should have been disabled but didn't exist

View file

@ -70,8 +70,8 @@ module.exports = class PlayLevelView extends View
$('body').append($("<img src='http://code.org/api/hour/begin_codecombat.png' style='visibility: hidden;'>"))
window.tracker?.trackEvent 'Hour of Code Begin', {}
@isEditorPreview = @getQueryVariable "dev"
@sessionID = @getQueryVariable "session"
@isEditorPreview = @getQueryVariable 'dev'
@sessionID = @getQueryVariable 'session'
$(window).on('resize', @onWindowResize)
@supermodel.once 'error', =>
@ -79,7 +79,14 @@ module.exports = class PlayLevelView extends View
@$el.html('<div class="alert">' + msg + '</div>')
@saveScreenshot = _.throttle @saveScreenshot, 30000
@load() unless @isEditorPreview
if @isEditorPreview
f = =>
@supermodel.shouldSaveBackups = (model) ->
model.constructor.className in ['Level', 'LevelComponent', 'LevelSystem']
@load() unless @levelLoader
setTimeout f, 100
else
@load()
# Save latest level played in local storage
if localStorage?

View file

@ -32,7 +32,7 @@
"firepad": "~0.1.2",
"marked": "~0.3.0",
"moment": "~2.5.0",
"aether": "~0.0.5",
"aether": "~0.0.7",
"underscore.string": "~2.3.3",
"firebase": "~1.0.2"
},

View file

@ -18,6 +18,7 @@ logging = require './server/commons/logging'
sprites = require './server/routes/sprites'
contact = require './server/routes/contact'
languages = require './server/routes/languages'
mail = require './server/routes/mail'
https = require 'https'
http = require 'http'
@ -82,6 +83,7 @@ contact.setupRoutes(app)
file.setupRoutes(app)
folder.setupRoutes(app)
languages.setupRoutes(app)
mail.setupRoutes(app)
# Some sort of cross-domain communication hack facebook requires
app.get('/channel.html', (req, res) ->

View file

@ -0,0 +1,19 @@
config = require '../../server_config'
module.exports.MAILCHIMP_LIST_ID = 'e9851239eb'
module.exports.MAILCHIMP_GROUP_ID = '4529'
module.exports.MAILCHIMP_GROUP_MAP =
announcement: 'Announcements'
tester: 'Adventurers'
level_creator: 'Artisans'
developer: 'Archmages'
article_editor: 'Scribes'
translator: 'Diplomats'
support: 'Ambassadors'
nodemailer = require 'nodemailer'
module.exports.transport = nodemailer.createTransport "SMTP",
service: config.mail.service
user: config.mail.username
pass: config.mail.password
authMethod: "LOGIN"

View file

@ -4,8 +4,8 @@ LocalStrategy = require('passport-local').Strategy
User = require('../users/User')
UserHandler = require('../users/user_handler')
config = require '../../server_config'
nodemailer = require 'nodemailer'
errors = require '../commons/errors'
mail = require '../commons/mail'
module.exports.setupRoutes = (app) ->
passport.serializeUser((user, done) -> done(null, user._id))
@ -66,9 +66,8 @@ module.exports.setupRoutes = (app) ->
user.save (err) =>
return errors.serverError(res) if err
if config.isProduction
transport = createSMTPTransport()
options = createMailOptions req.body.email, user.get('passwordReset')
transport.sendMail options, (error, response) ->
mail.transport.sendMail options, (error, response) ->
if error
console.error "Error sending mail: #{error.message or error}"
return errors.serverError(res) if err
@ -104,13 +103,4 @@ createMailOptions = (receiver, password) ->
replyTo: config.mail.username
subject: "[CodeCombat] Password Reset"
text: "You can log into your account with: #{password}"
#html: message.replace '\n', '<br>\n'
createSMTPTransport = ->
return smtpTransport if smtpTransport
smtpTransport = nodemailer.createTransport "SMTP",
service: config.mail.service
user: config.mail.username
pass: config.mail.password
authMethod: "LOGIN"
smtpTransport
#

View file

@ -1,14 +1,13 @@
config = require '../../server_config'
winston = require 'winston'
nodemailer = require 'nodemailer'
mail = require '../commons/mail'
module.exports.setupRoutes = (app) ->
app.post '/contact', (req, res) ->
winston.info "Sending mail from #{req.body.email} saying #{req.body.message}"
if config.isProduction
transport = createSMTPTransport()
options = createMailOptions req.body.email, req.body.message, req.user
transport.sendMail options, (error, response) ->
mail.transport.sendMail options, (error, response) ->
if error
winston.error "Error sending mail: #{error.message or error}"
else
@ -17,21 +16,10 @@ module.exports.setupRoutes = (app) ->
createMailOptions = (sender, message, user) ->
# TODO: use email templates here
console.log 'text is now', "#{message}\n\n#{user.get('name')}\nID: #{user._id}"
options =
from: config.mail.username
to: config.mail.username
replyTo: sender
subject: "[CodeCombat] Feedback - #{sender}"
text: "#{message}\n\nUsername: #{user.get('name') or 'Anonymous'}\nID: #{user._id}"
#html: message.replace '\n', '<br>\n'
smtpTransport = null
createSMTPTransport = ->
return smtpTransport if smtpTransport
smtpTransport = nodemailer.createTransport "SMTP",
service: config.mail.service
user: config.mail.username
pass: config.mail.password
authMethod: "LOGIN"
smtpTransport
#html: message.replace '\n', '<br>\n'

61
server/routes/mail.coffee Normal file
View file

@ -0,0 +1,61 @@
mail = require '../commons/mail'
map = _.invert mail.MAILCHIMP_GROUP_MAP
User = require '../users/User.coffee'
errors = require '../commons/errors'
#request = require 'request'
config = require '../../server_config'
#badLog = (text) ->
# console.log text
# request.post 'http://requestb.in/1brdpaz1', { form: {log: text} }
module.exports.setupRoutes = (app) ->
app.all config.mail.mailchimpWebhook, (req, res) ->
post = req.body
# badLog("Got post data: #{JSON.stringify(post, null, '\t')}")
unless post.type in ['unsubscribe', 'profile']
res.send 'Bad post type'
return res.end()
unless post.data.email
res.send 'No email provided'
return res.end()
query = {'mailChimp.leid':post.data.web_id}
User.findOne query, (err, user) ->
return errors.serverError(res) if err
if not user
return errors.notFound(res)
handleProfileUpdate(user, post) if post.type is 'profile'
handleUnsubscribe(user) if post.type is 'unsubscribe'
user.updatedMailChimp = true # so as not to echo back to mailchimp
user.save (err) ->
return errors.serverError(res) if err
res.end('Success')
handleProfileUpdate = (user, post) ->
groups = post.data.merges.INTERESTS.split(', ')
groups = (map[g] for g in groups when map[g])
otherSubscriptions = (g for g in user.get('emailSubscriptions') when not mail.MAILCHIMP_GROUP_MAP[g])
groups = groups.concat otherSubscriptions
user.set 'emailSubscriptions', groups
fname = post.data.merges.FNAME
user.set('firstName', fname) if fname
lname = post.data.merges.LNAME
user.set('lastName', lname) if lname
user.set 'mailChimp.email', post.data.email
user.set 'mailChimp.euid', post.data.id
# badLog("Updating user object to: #{JSON.stringify(user.toObject(), null, '\t')}")
handleUnsubscribe = (user) ->
user.set 'emailSubscriptions', []
# badLog("Unsubscribing user object to: #{JSON.stringify(user.toObject(), null, '\t')}")

View file

@ -2,6 +2,7 @@ mongoose = require('mongoose')
jsonschema = require('./user_schema')
crypto = require('crypto')
{salt, isProduction} = require('../../server_config')
mail = require '../commons/mail'
sendwithus = require '../sendwithus'
@ -34,16 +35,17 @@ UserSchema.statics.updateMailChimp = (doc, callback) ->
existingProps = doc.get('mailChimp')
emailChanged = (not existingProps) or existingProps?.email isnt doc.get('email')
emailSubs = doc.get('emailSubscriptions')
newGroups = (groupingMap[name] for name in emailSubs when groupingMap[name]?)
gm = mail.MAILCHIMP_GROUP_MAP
newGroups = (gm[name] for name in emailSubs when gm[name]?)
if (not existingProps) and newGroups.length is 0
return callback?() # don't add totally unsubscribed people to the list
subsChanged = doc.currentSubscriptions isnt JSON.stringify(emailSubs)
return callback?() unless emailChanged or subsChanged
params = {}
params.id = MAILCHIMP_LIST_ID
params.id = mail.MAILCHIMP_LIST_ID
params.email = if existingProps then {leid:existingProps.leid} else {email:doc.get('email')}
params.merge_vars = { groupings: [ {id: MAILCHIMP_GROUP_ID, groups: newGroups} ] }
params.merge_vars = { groupings: [ {id: mail.MAILCHIMP_GROUP_ID, groups: newGroups} ] }
params.update_existing = true
params.double_optin = false
@ -79,18 +81,6 @@ UserSchema.pre('save', (next) ->
next()
)
MAILCHIMP_LIST_ID = 'e9851239eb'
MAILCHIMP_GROUP_ID = '4529'
groupingMap =
announcement: 'Announcements'
tester: 'Adventurers'
level_creator: 'Artisans'
developer: 'Archmages'
article_editor: 'Scribes'
translator: 'Diplomats'
support: 'Ambassadors'
UserSchema.post 'save', (doc) ->
UserSchema.statics.updateMailChimp(doc)

View file

@ -27,6 +27,7 @@ config.mail.service = process.env.COCO_MAIL_SERVICE_NAME || "Zoho";
config.mail.username = process.env.COCO_MAIL_SERVICE_USERNAME || "";
config.mail.password = process.env.COCO_MAIL_SERVICE_PASSWORD || "";
config.mail.mailchimpAPIKey = process.env.COCO_MAILCHIMP_API_KEY || '';
config.mail.mailchimpWebhook = process.env.COCO_MAILCHIMP_WEBHOOK || '/mail/webhook';
config.mail.sendwithusAPIKey = process.env.COCO_SENDWITHUS_API_KEY || '';
config.salt = process.env.COCO_SALT || 'pepper';