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

View file

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

View file

@ -1,6 +1,6 @@
{backboneFailure, genericFailure} = require 'lib/errors' {backboneFailure, genericFailure} = require 'lib/errors'
User = require 'models/User' User = require 'models/User'
{saveObjectToStorage, loadObjectFromStorage} = require 'lib/storage' storage = require 'lib/storage'
module.exports.CURRENT_USER_KEY = CURRENT_USER_KEY = 'whoami' module.exports.CURRENT_USER_KEY = CURRENT_USER_KEY = 'whoami'
BEEN_HERE_BEFORE_KEY = 'beenHereBefore' BEEN_HERE_BEFORE_KEY = 'beenHereBefore'
@ -10,7 +10,7 @@ module.exports.createUser = (userObject, failure=backboneFailure) ->
user.save({}, { user.save({}, {
error: failure, error: failure,
success: (model) -> success: (model) ->
saveObjectToStorage(CURRENT_USER_KEY, model) storage.save(CURRENT_USER_KEY, model)
window.location.reload() window.location.reload()
}) })
@ -21,7 +21,7 @@ module.exports.loginUser = (userObject, failure=genericFailure) ->
password:userObject.password password:userObject.password
}, },
(model) -> (model) ->
saveObjectToStorage(CURRENT_USER_KEY, model) storage.save(CURRENT_USER_KEY, model)
window.location.reload() window.location.reload()
) )
jqxhr.fail(failure) jqxhr.fail(failure)
@ -29,7 +29,7 @@ module.exports.loginUser = (userObject, failure=genericFailure) ->
module.exports.logoutUser = -> module.exports.logoutUser = ->
FB?.logout?() FB?.logout?()
res = $.post('/auth/logout', {}, -> res = $.post('/auth/logout', {}, ->
saveObjectToStorage(CURRENT_USER_KEY, null) storage.save(CURRENT_USER_KEY, null)
window.location.reload() window.location.reload()
) )
res.fail(genericFailure) res.fail(genericFailure)
@ -38,7 +38,7 @@ init = ->
# Load the user from local storage, and refresh it from the server. # Load the user from local storage, and refresh it from the server.
# Also refresh and cache the gravatar info. # Also refresh and cache the gravatar info.
storedUser = loadObjectFromStorage(CURRENT_USER_KEY) storedUser = storage.load(CURRENT_USER_KEY)
firstTime = not storedUser firstTime = not storedUser
module.exports.me = window.me = new User(storedUser) module.exports.me = window.me = new User(storedUser)
me.url = -> '/auth/whoami' me.url = -> '/auth/whoami'
@ -50,14 +50,14 @@ init = ->
# Assign testGroupNumber to returning visitors; new ones in server/handlers/user # Assign testGroupNumber to returning visitors; new ones in server/handlers/user
me.set 'testGroupNumber', Math.floor(Math.random() * 256) me.set 'testGroupNumber', Math.floor(Math.random() * 256)
me.save() me.save()
saveObjectToStorage(CURRENT_USER_KEY, me.attributes) storage.save(CURRENT_USER_KEY, me.attributes)
me.loadGravatarProfile() if me.get('email') me.loadGravatarProfile() if me.get('email')
me.on('sync', userSynced) me.on('sync', userSynced)
userSynced = (user) -> userSynced = (user) ->
Backbone.Mediator.publish('me:synced', {me:user}) Backbone.Mediator.publish('me:synced', {me:user})
saveObjectToStorage(CURRENT_USER_KEY, user) storage.save(CURRENT_USER_KEY, user)
init() init()
@ -71,7 +71,7 @@ Backbone.Mediator.subscribe('level-set-volume', onSetVolume, module.exports)
trackFirstArrival = -> trackFirstArrival = ->
# will have to filter out users who log in with existing accounts separately # 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 # 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 return if beenHereBefore
window.tracker?.trackEvent 'First Arrived' 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) s = localStorage.getItem(key)
return null unless s return null unless s
try try
@ -8,6 +8,8 @@ module.exports.loadObjectFromStorage = (key) ->
console.warning('error loading from storage', key) console.warning('error loading from storage', key)
return null return null
module.exports.saveObjectToStorage = (key, value) -> module.exports.save = (key, value) ->
s = JSON.stringify(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. # Target is either just a {x, y} pos or a display object with {x, y} that might change; surface coordinates.
time = 0 if @instant time = 0 if @instant
newTarget ?= {x:0, y:0} newTarget ?= {x:0, y:0}
newTarget = (@newTarget or @target) if @locked
newZoom = Math.min((Math.max @minZoom, newZoom), MAX_ZOOM) 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 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 = module.exports.thangNames = thangNames =
"Soldier": [ "Soldier M": [
"William" "William"
"Lucas" "Lucas"
"Marcus" "Marcus"
@ -45,16 +45,18 @@ module.exports.thangNames = thangNames =
"Sterling" "Sterling"
"Alistair" "Alistair"
"Remy" "Remy"
"Lana"
"Stormy" "Stormy"
"Halle" "Halle"
"Sage"
]
"Soldier F": [
"Sarah" "Sarah"
"Alexandra" "Alexandra"
"Holly" "Holly"
"Trinity" "Trinity"
"Nikita" "Nikita"
"Alana" "Alana"
"Sage" "Lana"
] ]
"Peasant": [ "Peasant": [
"Yorik" "Yorik"
@ -77,7 +79,7 @@ module.exports.thangNames = thangNames =
"Bernadette" "Bernadette"
"Hershell" "Hershell"
] ]
"Archer": [ "Archer F": [
"Phoebe" "Phoebe"
"Mira" "Mira"
"Agapi" "Agapi"
@ -98,13 +100,15 @@ module.exports.thangNames = thangNames =
"Clare" "Clare"
"Rowan" "Rowan"
"Omar" "Omar"
"Brian"
"Cole"
"Alden" "Alden"
"Cairn" "Cairn"
"Jensen" "Jensen"
] ]
"Ogre Munchkin": [ "Archer M": [
"Brian"
"Cole"
]
"Ogre Munchkin M": [
"Brack" "Brack"
"Gort" "Gort"
"Weeb" "Weeb"
@ -128,7 +132,10 @@ module.exports.thangNames = thangNames =
"Snortt" "Snortt"
"Kog" "Kog"
] ]
"Ogre": [ "Ogre Munchkin F": [
]
"Ogre M": [
"Krogg" "Krogg"
"Dronck" "Dronck"
"Trogdor" "Trogdor"
@ -140,6 +147,9 @@ module.exports.thangNames = thangNames =
"Nareng" "Nareng"
"Morthrug" "Morthrug"
"Glonc" "Glonc"
]
"Ogre F": [
] ]
"Ogre Brawler": [ "Ogre Brawler": [
"Grul'thock" "Grul'thock"

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
saving: "Guardando..." saving: "Guardando..."
sending: "Enviando..." sending: "Enviando..."
cancel: "Cancelar" cancel: "Cancelar"
# save: "Save" save: "Guardar"
delay_1_sec: "1 segundo" delay_1_sec: "1 segundo"
delay_3_sec: "3 segundos" delay_3_sec: "3 segundos"
delay_5_sec: "5 segundos" delay_5_sec: "5 segundos"
@ -31,15 +31,15 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
about: "Sobre nosotros" about: "Sobre nosotros"
contact: "Contacta" contact: "Contacta"
twitter_follow: "Síguenos" twitter_follow: "Síguenos"
# employers: "Employers" employers: "Empresas"
# versions: versions:
# save_version_title: "Save New Version" save_version_title: "Guardar nueva versión"
# new_major_version: "New Major Version" new_major_version: "Nueva versión principal"
# cla_prefix: "To save changes, first you must agree to our" cla_prefix: "Para guardar los cambios, primero debes aceptar nuestro"
# cla_url: "CLA" cla_url: "CLA"
# cla_suffix: "." cla_suffix: "."
# cla_agree: "I AGREE" cla_agree: "De acuerdo"
login: login:
sign_up: "Crear una cuenta" sign_up: "Crear una cuenta"
@ -49,10 +49,10 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
recover: recover:
recover_account_title: "recuperar cuenta" recover_account_title: "recuperar cuenta"
# send_password: "Send Recovery Password" send_password: "Enviar recuperación de contraseña"
signup: 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!" description: "Es gratis. Solo necesitamos un par de cosas y listo para comenzar!"
email_announcements: "Recibir noticias por correo electrónico" email_announcements: "Recibir noticias por correo electrónico"
coppa: "Soy mayor de 13 o de fuera de los Estados Unidos" 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: play:
choose_your_level: "Elige tu nivel" choose_your_level: "Elige tu nivel"
adventurer_prefix: "Puedes elegir cualquier pantalla o charlar en " 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." adventurer_suffix: "sobre ello."
campaign_beginner: "Campaña de Principiante" campaign_beginner: "Campaña de Principiante"
campaign_beginner_description: "... en la que aprenderás la magia de la programación." 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: diplomat_suggestion:
title: "¡Ayuda a traducir CodeCombat!" title: "¡Ayuda a traducir CodeCombat!"
sub_heading: "Necesitamos tus habilidades lingüisticas." 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." 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." 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" learn_more: "Aprende más sobre ser un Diplomático"
subscribe_as_diplomat: "Suscríbete como Diplomático" subscribe_as_diplomat: "Suscríbete como Diplomático"
# wizard_settings: wizard_settings:
# title: "Wizard Settings" title: "Ajustes del mago"
# customize_avatar: "Customize Your Avatar" customize_avatar: "Personaliza tu Avatar"
account_settings: account_settings:
title: "Ajustes de la cuenta" title: "Ajustes de la cuenta"
@ -122,7 +122,7 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
new_password_verify: "Verificar" new_password_verify: "Verificar"
email_subscriptions: "Suscripciones de correo electrónico" email_subscriptions: "Suscripciones de correo electrónico"
email_announcements: "Noticias" 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." email_announcements_description: "Recibe correos electrónicos con las últimas noticias y desarrollos de CodeCombat."
contributor_emails: "Correos para colaboradores" contributor_emails: "Correos para colaboradores"
contribute_prefix: "¡Buscamos gente que se una a nuestro comunidad! Comprueba la " 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_for_suffix: ""
profile: "Perfil" profile: "Perfil"
user_not_found: "No se encontró al usuario. ¿Comprueba la URL?" 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_not_found_email_suffix: "."
gravatar_signup_prefix: "Suscribete " gravatar_signup_prefix: "¡Suscribete en "
gravatar_signup_suffix: " para ponerte en marcha!" 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_not_found_other: "Vaya, no hay un perfil asociado a la dirección de correo electrónico de esta persona."
gravatar_contact: "Contacto" gravatar_contact: "Contacto"
gravatar_websites: "Paginas web" gravatar_websites: "Paginas web"
gravatar_accounts: "Como se vé en" gravatar_accounts: "Como se vé en"
gravatar_profile_link: "Prefil de Gravatar completo" gravatar_profile_link: "Perfil de Gravatar completo"
play_level: play_level:
level_load_error: "No se pudo cargar el nivel." 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_select_a_thang: "Selecciona a alguien para "
tome_available_spells: "Hechizos disponibles" tome_available_spells: "Hechizos disponibles"
hud_continue: "Continuar (pulsa Shift+Space)" hud_continue: "Continuar (pulsa Shift+Space)"
# spell_saved: "Spell Saved" spell_saved: "Hechizo guardado"
# admin: # admin:
# av_title: "Admin Views" # av_title: "Admin Views"
@ -203,29 +203,29 @@ module.exports = nativeDescription: "español (ES)", englishDescription: "Spanis
# u_title: "User List" # u_title: "User List"
# lg_title: "Latest Games" # lg_title: "Latest Games"
# editor: editor:
# main_title: "CodeCombat Editors" # main_title: "CodeCombat Editors"
# main_description: "Build your own levels, campaigns, units and educational content. We provide all the tools you need!" main_description: "Construye tus propios niveles, campañas, unidades y contenido educativo. ¡Nosotros te ofrecemos todas las herramientas que necesitas!"
# article_title: "Article Editor" 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." # 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_title: "Thang Editor"
# thang_description: "Build units, defining their default logic, graphics and audio. Currently only supports importing Flash exported vector graphics." # 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!" # 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, " # 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!" contact_us: "¡Contacta con nosotros!"
# hipchat_prefix: "You can also find us in our" hipchat_prefix: "También puedes encontrarnos en nuestra"
# hipchat_url: "HipChat room." # hipchat_url: "sala de HipChat."
# level_some_options: "Some Options?" # level_some_options: "Some Options?"
# level_tab_thangs: "Thangs" # level_tab_thangs: "Thangs"
# level_tab_scripts: "Scripts" # level_tab_scripts: "Scripts"
# level_tab_settings: "Settings" level_tab_settings: "Ajustes"
# level_tab_components: "Components" # level_tab_components: "Components"
# level_tab_systems: "Systems" # level_tab_systems: "Systems"
# level_tab_thangs_title: "Current Thangs" # level_tab_thangs_title: "Current Thangs"
# level_tab_thangs_conditions: "Starting Conditions" # level_tab_thangs_conditions: "Starting Conditions"
# level_tab_thangs_add: "Add Thangs" # level_tab_thangs_add: "Add Thangs"
# level_settings_title: "Settings" level_settings_title: "Ajustes"
# level_component_tab_title: "Current Components" # level_component_tab_title: "Current Components"
# level_component_btn_new: "Create New Component" # level_component_btn_new: "Create New Component"
# level_systems_tab_title: "Current Systems" # 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_title: "Create New Component"
# new_component_field_system: "System" # new_component_field_system: "System"
# article: article:
# edit_btn_preview: "Preview" edit_btn_preview: "Vista preliminar"
# edit_article_title: "Edit Article" edit_article_title: "Editar artículo"
# general: general:
# and: "and" and: "y"
or: "o" or: "o"
# name: "Name" name: "Nombre"
# body: "Body" # body: "Body"
# version: "Version" version: "Versión"
# commit_msg: "Commit Message" # commit_msg: "Commit Message"
# version_history_for: "Version History for: " # version_history_for: "Version History for: "
# results: "Results" results: "Resultados"
# description: "Description" description: "Descripción"
email: "Correo electrónico" email: "Correo electrónico"
message: "Mensaje" message: "Mensaje"
# about: about:
# who_is_codecombat: "Who is CodeCombat?" who_is_codecombat: "¿Qué es CodeCombat?"
# why_codecombat: "Why CodeCombat?" why_codecombat: "¿Por qué CodeCombat?"
# who_description_prefix: "together started CodeCombat in 2013. We also created " # 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_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_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_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_prefix: "De eso va la programación. Tiene que ser divertido. No divertido como:"
# why_paragraph_3_italic: "yay a badge" why_paragraph_3_italic: "¡bien una insignia!,"
# why_paragraph_3_center: "but fun like" why_paragraph_3_center: "sino más bien como:"
# why_paragraph_3_italic_caps: "NO MOM I HAVE TO FINISH THE LEVEL!" why_paragraph_3_italic_caps: "¡NO MAMA, TENGO QUE TERMINAR EL NIVEL!"
# 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_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: "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_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: "And hey, it's free. " why_ending: "Y, oye, es gratis. "
# why_ending_url: "Start wizarding now!" why_ending_url: "Comienza a hacer magia ¡ya!"
# george_description: "CEO, business guy, web designer, game designer, and champion of beginning programmers everywhere." # 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." # 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." # 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." # 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." # michael_description: "Programmer, sys-admin, and undergrad technical wunderkind, Michael is the person keeping our servers online."
# legal: legal:
# page_title: "Legal" page_title: "Legal"
# opensource_intro: "CodeCombat is free to play and completely open source." opensource_intro: "CodeCombat es gratis y totalmente open source."
# opensource_description_prefix: "Check out " opensource_description_prefix: "Echa un vistazo a "
# github_url: "our GitHub" github_url: "nuestro GitHub"
# opensource_description_center: "and help out if you like! CodeCombat is built on dozens of open source projects, and we love them. See " opensource_description_center: "y ayúdanos si quieres. CodeCombat está desarrollado sobre docenas de proyectos open source, y nos encantana. Mira "
# archmage_wiki_url: "our Archmage wiki" archmage_wiki_url: "nuestra wiki del Archimago"
# opensource_description_suffix: "for a list of the software that makes this game possible." opensource_description_suffix: "para encontrar una lista del software que hace este juego posible."
# practices_title: "Respectful Best Practices" practices_title: "Prácticas respetuosas"
# practices_description: "These are our promises to you, the player, in slightly less legalese." practices_description: "Esto es lo que te prometemos a ti, el jugador, sin usar mucha jerga legal."
# privacy_title: "Privacy" privacy_title: "Privacidad"
# 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." 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: "Security" security_title: "Seguridad"
# 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." 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: "Email" email_title: "Correo electrónico"
# email_description_prefix: "We will not inundate you with spam. Through" email_description_prefix: "No te inundaremos con spam. Mediante"
# email_settings_url: "your email settings" email_settings_url: "tus ajustes de correo electrónico"
# email_description_suffix: "or through links in the emails we send, you can change your preferences and easily unsubscribe at any time." 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: "Cost" cost_title: "Precio"
# 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:" 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: "Recruitment" recruitment_title: "Contratación"
# recruitment_description_prefix: "Here on CodeCombat, you're going to become a powerful wizardnot just in the game, but also in real life." 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: "No one can hire programmers fast enough" url_hire_programmers: "Nadie puede contratar programadores con la suficiente rapidez"
# 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_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: "a lot" recruitment_description_italic: "un montón."
# recruitment_description_ending: "the site remains free and everybody's happy. That's the plan." recruitment_description_ending: "La web permanece gratuita y todo el mundo es feliz. Ese es el plan."
# copyrights_title: "Copyrights and Licenses" copyrights_title: "Copyrights y Licencias"
# contributor_title: "Contributor License Agreement" contributor_title: "Acuerdo de Licencia del Colaborador"
# contributor_description_prefix: "All contributions, both on the site and on our GitHub repository, are subject to our" contributor_description_prefix: "Todas las colaboraciones, tanto en la web como en nuestro repositorio de GitHub, están sujetas a nuestro"
# cla_url: "CLA" cla_url: "CLA"
# contributor_description_suffix: "to which you should agree before contributing." contributor_description_suffix: "con el que deberás estar de acuerdo antes de colaborar."
# code_title: "Code - MIT" # 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" # 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." # 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_title: "Arte/Música - Creative Commons "
# art_description_prefix: "All common content is available under the" art_description_prefix: "Todo el contenido común está disponible bajo la"
# cc_license_url: "Creative Commons Attribution 4.0 International License" # 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_description_suffix: "Common content is anything made generally available by CodeCombat for the purpose of creating Levels. This includes:"
# art_music: "Music" art_music: "Música"
# art_sound: "Sound" art_sound: "Sonido"
# art_artwork: "Artwork" # art_artwork: "Artwork"
# art_sprites: "Sprites" # art_sprites: "Sprites"
# art_other: "Any and all other non-code creative works that are made available when creating Levels." # 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." # 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." # 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: contribute:
# page_title: "Contributing" page_title: "Colaborar"
# character_classes_title: "Character Classes" # character_classes_title: "Character Classes"
# introduction_desc_intro: "We have high hopes for CodeCombat." # 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, " # 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:" # translating_diplomats: "Our Translating Diplomats:"
# helpful_ambassadors: "Our Helpful Ambassadors:" # helpful_ambassadors: "Our Helpful Ambassadors:"
# classes: classes:
# archmage_title: "Archmage" archmage_title: "Archimago"
# archmage_title_description: "(Coder)" # archmage_title_description: "(Coder)"
# artisan_title: "Artisan" # artisan_title: "Artisan"
# artisan_title_description: "(Level Builder)" # artisan_title_description: "(Level Builder)"

View file

@ -205,46 +205,46 @@ module.exports = nativeDescription: "magyar", englishDescription: "Hungarian", t
editor: editor:
main_title: "CodeCombat szerkesztők" 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!" 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: "Article Editor" article_title: "Cikk szerkesztő"
# article_description: "Write articles that give players overviews of programming concepts which can be used across a variety of levels and campaigns." 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: "Thang Editor" thang_title: "Eszköz szerkesztő"
# thang_description: "Build units, defining their default logic, graphics and audio. Currently only supports importing Flash exported vector graphics." 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: "Level Editor" level_title: "Pálya szerkesztő"
# level_description: "Includes the tools for scripting, uploading audio, and constructing custom logic to create all sorts of levels. Everything we use ourselves!" 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: "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, " 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: "contact us!" contact_us: "lépj kapcsolatba velünk!"
# hipchat_prefix: "You can also find us in our" hipchat_prefix: "Megtalálhatsz bennünket a "
# hipchat_url: "HipChat room." hipchat_url: "HipChat szobában."
# level_some_options: "Some Options?" level_some_options: "Néhány beállítás?"
# level_tab_thangs: "Thangs" level_tab_thangs: "Eszközök"
# level_tab_scripts: "Scripts" level_tab_scripts: "Kódok"
# level_tab_settings: "Settings" level_tab_settings: "Beállítások"
# level_tab_components: "Components" level_tab_components: "Komponensek"
# level_tab_systems: "Systems" level_tab_systems: "Rendszerek"
# level_tab_thangs_title: "Current Thangs" level_tab_thangs_title: "Jelenlegi eszközök"
# level_tab_thangs_conditions: "Starting Conditions" level_tab_thangs_conditions: "Kezdő feltételek"
# level_tab_thangs_add: "Add Thangs" level_tab_thangs_add: "Eszköz hozzáadása"
# level_settings_title: "Settings" level_settings_title: "Beállítások"
# level_component_tab_title: "Current Components" level_component_tab_title: "Jelenlegi komponensek"
# level_component_btn_new: "Create New Component" level_component_btn_new: "Új komponens készítése"
# level_systems_tab_title: "Current Systems" level_systems_tab_title: "Jelenlegi rendszerek"
# level_systems_btn_new: "Create New System" level_systems_btn_new: "Új rendszer készítése"
# level_systems_btn_add: "Add System" level_systems_btn_add: "Rendszer hozzáadása"
# level_components_title: "Back to All Thangs" level_components_title: "Vissza az összes eszközhöz"
# level_components_type: "Type" level_components_type: "Típus"
# level_component_edit_title: "Edit Component" level_component_edit_title: "Komponens szerkesztése"
# level_system_edit_title: "Edit System" level_system_edit_title: "Rendszer szerkesztése"
# create_system_title: "Create New System" create_system_title: "Új rendszer készítése"
# new_component_title: "Create New Component" new_component_title: "Új komponens készítése"
# new_component_field_system: "System" new_component_field_system: "Rendszer"
# article: article:
# edit_btn_preview: "Preview" edit_btn_preview: "Előnézet"
# edit_article_title: "Edit Article" edit_article_title: "Cikk szerkesztése"
general: general:
# and: "and" and: "és"
or: "vagy " or: "vagy "
name: "Név" name: "Név"
# body: "Body" # body: "Body"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,19 +21,26 @@
.save-status .save-status
display: none display: none
position: relative position: absolute
padding-top: 2px bottom: 2%
padding-left: 10px left: 1%
.firepad .firepad
width: 100%
height: 100%
@include box-sizing(border-box) @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 .ace_editor
@include box-sizing(border-box) @include box-sizing(border-box)
margin-top: 40px // When Firepad isn't active, .ace_editor needs the width/height set itself.
width: 100% width: 98%
height: 83% height: 83%
height: -webkit-calc(100% - 60px - 40px) height: -webkit-calc(100% - 60px - 40px)
height: calc(100% - 60px - 40px) height: calc(100% - 60px - 40px)
@ -61,9 +68,9 @@
.executing, .executed, .problem-marker-info, .problem-marker-warning, .problem-marker-error .executing, .executed, .problem-marker-info, .problem-marker-warning, .problem-marker-error
position: absolute position: absolute
.executing .executing
background-color: rgba(216, 255, 255, 0.55) background-color: rgba(216, 255, 255, 0.85)
.executed .executed
background-color: rgba(216, 255, 255, 0.25) background-color: rgba(245, 255, 6, 0.18)
.problem-marker-info .problem-marker-info
background-color: rgba(96, 63, 84, 0.25) background-color: rgba(96, 63, 84, 0.25)
.problem-marker-warning .problem-marker-warning
@ -79,7 +86,7 @@
.ace_marker-layer .ace_marker-layer
.ace_bracket .ace_bracket
// Override faint gray // Override faint gray
border-color: #8FF border-color: #BFF
.ace_identifier .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" @import "../../../bootstrap/mixins"
.spell-toolbar-view .spell-toolbar-view
position: absolute position: relative
z-index: 2
top: 2px
left: 5px
box-sizing: border-box box-sizing: border-box
padding-left: 150px margin: 4px 1%
height: 36px height: 45px
width: 95% width: 97%
width: -webkit-calc(95% - 5px) //background-color: rgba(100, 45, 210, 0.15)
width: calc(95% - 5px)
background-color: rgba(100, 45, 210, 0.05)
.spell-progress .flow
position: relative &:hover .spell-progress
height: 100% opacity: 1
width: 50%
display: inline-block
.progress .spell-progress
position: absolute position: absolute
left: 0px height: 100%
top: 8px width: 40%
bottom: 0px left: 45%
width: 100% display: inline-block
cursor: pointer cursor: pointer
overflow: visible box-sizing: border-box
opacity: 0.25
.bar
@include transition(width .0s linear) .progress
position: relative position: absolute
left: 0px
top: 12.5px
bottom: 0px
width: 100%
height: 4px
overflow: visible
pointer-events: none pointer-events: none
background-color: #67A4C8
width: 50% .bar
@include transition(width .0s linear)
.scrubber-handle position: relative
position: absolute
pointer-events: none pointer-events: none
right: -16px background: linear-gradient(#2c3e5f, #2c3e5f 16%, #3a537f 16%, #3a537f 83%, #2c3e5f 84%, #2c3e5f)
top: -7px width: 50%
background: transparent url(/images/level/playback_thumb.png) pointer-events: none
width: 32px
height: 32px .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 .steppers
// I don't know, I can figure this out for real later position: absolute
margin: -26px 0 0 18px z-index: 2
width: 10%
.metrics right: 2%
display: inline-block box-sizing: border-box
margin: -30px 0 0 10px opacity: 0.25
vertical-align: middle
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. p(data-i18n="account_settings.not_logged_in") Log in or create an account to change your settings.
else 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 ul.nav.nav-tabs#settings-tabs
li li

View file

@ -1,6 +1,7 @@
extends /templates/base extends /templates/base
block content 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-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 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} span.level-title #{level.attributes.name}
.level-control-buttons .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.save").btn.btn-primary#commit-level-start-button Save
button(data-i18n="common.fork").btn.btn-primary#fork-level-start-button Fork button(data-i18n="common.fork").btn.btn-primary#fork-level-start-button Fork
.btn-group.play-button-group .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") button.btn.btn-primary#save-button(data-toggle="coco-modal", data-target="modal/save_version")
| Save | 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}" 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 div.btn-group.cast-button-group
.button-progress-overlay .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.btn-large.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-options-button.dropdown-toggle(data-toggle="dropdown")
i.icon-cog.icon-white i.icon-cog.icon-white
ul.dropdown-menu.autocast-delays ul.dropdown-menu.autocast-delays

View file

@ -1,22 +1,25 @@
.spell-progress .flow
.progress
.bar
.scrubber-handle
.btn-group .spell-progress
button.btn.btn-mini.btn-inverse.banner.step-backward(title="Ctrl/Cmd + Alt + [: Step Backward") .progress
i.icon-arrow-left.icon-white .bar
button.btn.btn-mini.btn-inverse.banner.step-forward(title="Ctrl/Cmd + Alt + ]: Step Forward") .scrubber-handle
i.icon-arrow-right.icon-white
.btn-group.steppers
.metrics button.btn.btn-mini.btn-inverse.banner.step-backward(title="Ctrl/Cmd + Alt + [: Step Backward")
code.statements-metric i.icon-arrow-left.icon-white
span.metric.statement-index button.btn.btn-mini.btn-inverse.banner.step-forward(title="Ctrl/Cmd + Alt + ]: Step Forward")
| / i.icon-arrow-right.icon-white
span.metric.statements-executed
span.metric.statements-executed-total .metrics
| .statements-metric
code.calls-metric | Statement
span.metric.call-index span.metric.statement-index
| / | /
span.metric.calls-executed 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) -> constructor: (options, @articleID) ->
super options super options
@article = new Article(_id: @articleID) @article = new Article(_id: @articleID)
@article.saveBackups = true
@article.fetch() @article.fetch()
@article.once('sync', @onArticleSync) @article.once('sync', @onArticleSync)
@article.on('schema-loaded', @buildTreema) @article.on('schema-loaded', @buildTreema)
@ -43,6 +44,8 @@ module.exports = class ArticleEditView extends View
@treema.build() @treema.build()
pushChangesToPreview: => pushChangesToPreview: =>
for key, value of @treema.data
@article.set(key, value)
return unless @treema and @preview return unless @treema and @preview
m = marked(@treema.data.body) m = marked(@treema.data.body)
b = $(@preview.document.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 haveThisComponent.push thang.id if haveThisComponent.length < 100 # for performance when adding many Thangs
return if _.isEqual presentComponents, @presentComponents return if _.isEqual presentComponents, @presentComponents
@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 = treemaOptions =
supermodel: @supermodel supermodel: @supermodel
schema: {type: 'array', items: {type: 'object', format: 'level-component'}} 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 false if @levelsLoaded > 1
return true return true
@supermodel.shouldSaveBackups = (model) ->
model.constructor.className in ['Level', 'LevelComponent', 'LevelSystem']
@level = new Level _id: @levelID @level = new Level _id: @levelID
@level.once 'sync', @onLevelLoaded @level.once 'sync', @onLevelLoaded
@supermodel.populateModel @level @supermodel.populateModel @level

View file

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

View file

@ -39,6 +39,7 @@ module.exports = class ThangTypeEditView extends View
super options super options
@mockThang = _.cloneDeep(@mockThang) @mockThang = _.cloneDeep(@mockThang)
@thangType = new ThangType(_id: @thangTypeID) @thangType = new ThangType(_id: @thangTypeID)
@thangType.saveBackups = true
@thangType.fetch() @thangType.fetch()
@thangType.once('sync', @onThangTypeSync) @thangType.once('sync', @onThangTypeSync)
@refreshAnimation = _.debounce @refreshAnimation, 500 @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 delay ?= 5000
@setAutocastDelay delay @setAutocastDelay delay
attachTo: (spellView) ->
@$el.detach().prependTo(spellView.toolbarView.$el).show()
hookUpButtons: -> hookUpButtons: ->
# hook up cast button callbacks # hook up cast button callbacks
@castButton = $('.cast-button', @$el) @castButton = $('.cast-button', @$el)

View file

@ -56,7 +56,6 @@ module.exports = class Spell
createAether: (thang) -> createAether: (thang) ->
aetherOptions = aetherOptions =
thisValue: thang.createUserContext()
problems: problems:
jshint_W040: {level: "ignore"} jshint_W040: {level: "ignore"}
aether_MissingThis: {level: (if thang.requiresThis then 'error' else 'warning')} aether_MissingThis: {level: (if thang.requiresThis then 'error' else 'warning')}
@ -68,7 +67,7 @@ module.exports = class Spell
#callIndex: 0 #callIndex: 0
#timelessVariables: ['i'] #timelessVariables: ['i']
#statementIndex: 9001 #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 #console.log "Turning off includeFlow for", @spellKey
aetherOptions.includeFlow = false aetherOptions.includeFlow = false
aether = new Aether aetherOptions aether = new Aether aetherOptions

View file

@ -1,51 +1,76 @@
View = require 'views/kinds/CocoView' View = require 'views/kinds/CocoView'
template = require 'templates/play/level/tome/spell_debug' template = require 'templates/play/level/tome/spell_debug'
Range = ace.require("ace/range").Range 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 module.exports = class DebugView extends View
className: 'spell-debug-view' className: 'spell-debug-view'
template: template template: template
subscriptions: {}
subscriptions:
'god:new-world-created': 'onNewWorld'
events: {} events: {}
constructor: (options) -> constructor: (options) ->
super options super options
@ace = options.ace @ace = options.ace
@thang = options.thang
@variableStates = {} @variableStates = {}
afterRender: -> afterRender: ->
super() super()
@ace.on "mousemove", @onMouseMove @ace.on "mousemove", @onMouseMove
#@ace.on "click", onClick # same ACE API as mousemove
setVariableStates: (@variableStates) -> setVariableStates: (@variableStates) ->
@update() @update()
onMouseMove: (e) => onMouseMove: (e) =>
pos = e.getDocumentPosition() pos = e.getDocumentPosition()
column = pos.column endOfDoc = pos.row is @ace.getSession().getDocument().getLength() - 1
until column < 0 it = new TokenIterator e.editor.session, pos.row, pos.column
if token = e.editor.session.getTokenAt pos.row, column isIdentifier = (t) -> t and (t.type is 'identifier' or t.value is 'this')
break if token.type is 'identifier' while it.getCurrentTokenRow() is pos.row and not isIdentifier(token = it.getCurrentToken())
column = token.start - 1 it.stepBackward()
else break unless token
--column break if endOfDoc # Don't iterate backward on last line, since we might be way below.
if token?.type is 'identifier' and token.value of @variableStates if isIdentifier token
@variable = token.value # 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} @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 else
@variable = @markerRange = null @variableChain = @markerRange = null
@update() @update()
onMouseOut: (e) => onMouseOut: (e) =>
@variable = @markerRange = null @variableChain = @markerRange = null
@update() @update()
onNewWorld: (e) ->
@thang = @options.thang = e.world.thangMap[@thang.id] if @thang
update: -> update: ->
if @variable if @variableChain
value = @variableStates[@variable] {key, value} = @deserializeVariableChain @variableChain
@$el.find("code").text "#{@variable}: #{value}" @$el.find("code").text "#{key}: #{value}"
@$el.show().css(@pos) @$el.show().css(@pos)
else else
@$el.hide() @$el.hide()
@ -58,6 +83,28 @@ module.exports = class DebugView extends View
if @markerRange if @markerRange
@marker = @ace.getSession().addMarker @markerRange, "ace_bracket", "text" @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: -> destroy: ->
super() super()
@ace?.removeEventListener "mousemove", @onMouseMove @ace?.removeEventListener "mousemove", @onMouseMove

View file

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

View file

@ -51,7 +51,6 @@ module.exports = class SpellView extends View
else else
# needs to happen after the code generating this view is complete # needs to happen after the code generating this view is complete
setTimeout @onLoaded, 1 setTimeout @onLoaded, 1
@createDebugView()
createACE: -> createACE: ->
# Test themes and settings here: http://ace.ajax.org/build/kitchen-sink.html # 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.setShowPrintMargin false
@ace.setShowInvisibles false @ace.setShowInvisibles false
@ace.setBehavioursEnabled false @ace.setBehavioursEnabled false
@ace.setAnimatedScroll true
@toggleControls null, @writable @toggleControls null, @writable
@aceSession.selection.on 'changeCursor', @onCursorActivity @aceSession.selection.on 'changeCursor', @onCursorActivity
$(@ace.container).find('.ace_gutter').on 'click', '.ace_error, .ace_warning, .ace_info', @onAnnotationClick $(@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 @spell.loaded = true
Backbone.Mediator.publish 'tome:spell-loaded', spell: @spell Backbone.Mediator.publish 'tome:spell-loaded', spell: @spell
@eventsSuppressed = false # Now that the initial change is in, we can start running any changed code @eventsSuppressed = false # Now that the initial change is in, we can start running any changed code
@createToolbarView()
createDebugView: -> createDebugView: ->
@debugView = new SpellDebugView ace: @ace @debugView = new SpellDebugView ace: @ace, thang: @thang
@$el.append @debugView.render().$el.hide() @$el.append @debugView.render().$el.hide()
createToolbarView: -> createToolbarView: ->
@toolbarView = new SpellToolbarView ace: @ace @toolbarView = new SpellToolbarView ace: @ace
@$el.prepend @toolbarView.render().$el @$el.append @toolbarView.render().$el
onMouseOut: (e) -> onMouseOut: (e) ->
@debugView.onMouseOut e @debugView.onMouseOut e
@ -174,6 +175,8 @@ module.exports = class SpellView extends View
return if thang.id is @thang?.id return if thang.id is @thang?.id
@thang = thang @thang = thang
@spellThang = @spell.thangs[@thang.id] @spellThang = @spell.thangs[@thang.id]
@createDebugView() unless @debugView
@debugView.thang = @thang
@updateAether false, true @updateAether false, true
@highlightCurrentLine() @highlightCurrentLine()
@ -343,6 +346,7 @@ module.exports = class SpellView extends View
@spellHasChanged = true @spellHasChanged = true
onSessionWillSave: (e) -> onSessionWillSave: (e) ->
return unless @spellHasChanged
setTimeout(=> setTimeout(=>
unless @spellHasChanged unless @spellHasChanged
@$el.find('.save-status').finish().show().fadeOut(2000) @$el.find('.save-status').finish().show().fadeOut(2000)
@ -432,35 +436,32 @@ module.exports = class SpellView extends View
@debugView.setVariableStates {} @debugView.setVariableStates {}
@aceSession.removeGutterDecoration row, 'executing' for row in [0 ... @aceSession.getLength()] @aceSession.removeGutterDecoration row, 'executing' for row in [0 ... @aceSession.getLength()]
$(@ace.container).find('.ace_gutter-cell.executing').removeClass('executing') $(@ace.container).find('.ace_gutter-cell.executing').removeClass('executing')
unless executed.length if not executed.length or (@spell.name is "plan" and @spellThang.castAether.metrics.statementsExecuted < 20)
@toolbarView?.$el.hide() @toolbarView?.toggleFlow false
return return
unless @toolbarView or (@spell.name is "plan" and @spellThang.castAether.metrics.statementsExecuted < 20)
@createToolbarView()
lastExecuted = _.last executed lastExecuted = _.last executed
@toolbarView?.$el.show() @toolbarView?.toggleFlow true
statementIndex = Math.max 0, lastExecuted.length - 1 statementIndex = Math.max 0, lastExecuted.length - 1
@toolbarView?.setCallState states[currentCallIndex], statementIndex, currentCallIndex, @spellThang.castAether.metrics @toolbarView?.setCallState states[currentCallIndex], statementIndex, currentCallIndex, @spellThang.castAether.metrics
marked = {} marked = {}
lastExecuted = lastExecuted[0 .. @toolbarView.statementIndex] if @toolbarView?.statementIndex? lastExecuted = lastExecuted[0 .. @toolbarView.statementIndex] if @toolbarView?.statementIndex?
for state, i in lastExecuted 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' clazz = if i is lastExecuted.length - 1 then 'executing' else 'executed'
if clazz is 'executed' if clazz is 'executed'
key = state.range[0] + '_' + state.range[1] continue if marked[start.row]
continue if marked[key] > 2 # don't allow more than three of the same marker marked[start.row] = true
marked[key] ?= 0 markerType = "fullLine"
++marked[key]
else else
@debugView.setVariableStates state.variables @debugView.setVariableStates state.variables
#console.log "at", state.userInfo.time, "vars are now:", state.variables markerType = "text"
[start, end] = [offsetToPos(state.range[0]), offsetToPos(state.range[1])]
markerRange = new Range(start.row, start.column, end.row, end.column) markerRange = new Range(start.row, start.column, end.row, end.column)
markerRange.start = @aceDoc.createAnchor markerRange.start markerRange.start = @aceDoc.createAnchor markerRange.start
markerRange.end = @aceDoc.createAnchor markerRange.end markerRange.end = @aceDoc.createAnchor markerRange.end
markerRange.id = @aceSession.addMarker markerRange, clazz, "text" markerRange.id = @aceSession.addMarker markerRange, clazz, markerType
@markerRanges.push markerRange @markerRanges.push markerRange
@aceSession.addGutterDecoration start.row, clazz if clazz is 'executing' @aceSession.addGutterDecoration start.row, clazz if clazz is 'executing'
null
onAnnotationClick: -> onAnnotationClick: ->
alertBox = $("<div class='alert alert-info fade in'>#{msg}</div>") 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() @$el.find('#' + @spellTabView.id).after(@spellTabView.el).remove()
@spellView.setThang thang @spellView.setThang thang
@spellTabView.setThang thang @spellTabView.setThang thang
@castButton.$el.show() @castButton.attachTo @spellView
@thangList.$el.hide() @thangList.$el.hide()
@spellPaletteView = @insertSubView new SpellPaletteView thang: thang @spellPaletteView = @insertSubView new SpellPaletteView thang: thang
@spellPaletteView.toggleControls {}, @spellView.controlsEnabled # TODO: know when palette should have been disabled but didn't exist @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;'>")) $('body').append($("<img src='http://code.org/api/hour/begin_codecombat.png' style='visibility: hidden;'>"))
window.tracker?.trackEvent 'Hour of Code Begin', {} window.tracker?.trackEvent 'Hour of Code Begin', {}
@isEditorPreview = @getQueryVariable "dev" @isEditorPreview = @getQueryVariable 'dev'
@sessionID = @getQueryVariable "session" @sessionID = @getQueryVariable 'session'
$(window).on('resize', @onWindowResize) $(window).on('resize', @onWindowResize)
@supermodel.once 'error', => @supermodel.once 'error', =>
@ -79,7 +79,14 @@ module.exports = class PlayLevelView extends View
@$el.html('<div class="alert">' + msg + '</div>') @$el.html('<div class="alert">' + msg + '</div>')
@saveScreenshot = _.throttle @saveScreenshot, 30000 @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 # Save latest level played in local storage
if localStorage? if localStorage?

View file

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

View file

@ -18,6 +18,7 @@ logging = require './server/commons/logging'
sprites = require './server/routes/sprites' sprites = require './server/routes/sprites'
contact = require './server/routes/contact' contact = require './server/routes/contact'
languages = require './server/routes/languages' languages = require './server/routes/languages'
mail = require './server/routes/mail'
https = require 'https' https = require 'https'
http = require 'http' http = require 'http'
@ -82,6 +83,7 @@ contact.setupRoutes(app)
file.setupRoutes(app) file.setupRoutes(app)
folder.setupRoutes(app) folder.setupRoutes(app)
languages.setupRoutes(app) languages.setupRoutes(app)
mail.setupRoutes(app)
# Some sort of cross-domain communication hack facebook requires # Some sort of cross-domain communication hack facebook requires
app.get('/channel.html', (req, res) -> 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') User = require('../users/User')
UserHandler = require('../users/user_handler') UserHandler = require('../users/user_handler')
config = require '../../server_config' config = require '../../server_config'
nodemailer = require 'nodemailer'
errors = require '../commons/errors' errors = require '../commons/errors'
mail = require '../commons/mail'
module.exports.setupRoutes = (app) -> module.exports.setupRoutes = (app) ->
passport.serializeUser((user, done) -> done(null, user._id)) passport.serializeUser((user, done) -> done(null, user._id))
@ -66,9 +66,8 @@ module.exports.setupRoutes = (app) ->
user.save (err) => user.save (err) =>
return errors.serverError(res) if err return errors.serverError(res) if err
if config.isProduction if config.isProduction
transport = createSMTPTransport()
options = createMailOptions req.body.email, user.get('passwordReset') options = createMailOptions req.body.email, user.get('passwordReset')
transport.sendMail options, (error, response) -> mail.transport.sendMail options, (error, response) ->
if error if error
console.error "Error sending mail: #{error.message or error}" console.error "Error sending mail: #{error.message or error}"
return errors.serverError(res) if err return errors.serverError(res) if err
@ -104,13 +103,4 @@ createMailOptions = (receiver, password) ->
replyTo: config.mail.username replyTo: config.mail.username
subject: "[CodeCombat] Password Reset" subject: "[CodeCombat] Password Reset"
text: "You can log into your account with: #{password}" 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' config = require '../../server_config'
winston = require 'winston' winston = require 'winston'
nodemailer = require 'nodemailer' mail = require '../commons/mail'
module.exports.setupRoutes = (app) -> module.exports.setupRoutes = (app) ->
app.post '/contact', (req, res) -> app.post '/contact', (req, res) ->
winston.info "Sending mail from #{req.body.email} saying #{req.body.message}" winston.info "Sending mail from #{req.body.email} saying #{req.body.message}"
if config.isProduction if config.isProduction
transport = createSMTPTransport()
options = createMailOptions req.body.email, req.body.message, req.user options = createMailOptions req.body.email, req.body.message, req.user
transport.sendMail options, (error, response) -> mail.transport.sendMail options, (error, response) ->
if error if error
winston.error "Error sending mail: #{error.message or error}" winston.error "Error sending mail: #{error.message or error}"
else else
@ -17,21 +16,10 @@ module.exports.setupRoutes = (app) ->
createMailOptions = (sender, message, user) -> createMailOptions = (sender, message, user) ->
# TODO: use email templates here # TODO: use email templates here
console.log 'text is now', "#{message}\n\n#{user.get('name')}\nID: #{user._id}"
options = options =
from: config.mail.username from: config.mail.username
to: config.mail.username to: config.mail.username
replyTo: sender replyTo: sender
subject: "[CodeCombat] Feedback - #{sender}" subject: "[CodeCombat] Feedback - #{sender}"
text: "#{message}\n\nUsername: #{user.get('name') or 'Anonymous'}\nID: #{user._id}" text: "#{message}\n\nUsername: #{user.get('name') or 'Anonymous'}\nID: #{user._id}"
#html: message.replace '\n', '<br>\n' #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

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') jsonschema = require('./user_schema')
crypto = require('crypto') crypto = require('crypto')
{salt, isProduction} = require('../../server_config') {salt, isProduction} = require('../../server_config')
mail = require '../commons/mail'
sendwithus = require '../sendwithus' sendwithus = require '../sendwithus'
@ -34,16 +35,17 @@ UserSchema.statics.updateMailChimp = (doc, callback) ->
existingProps = doc.get('mailChimp') existingProps = doc.get('mailChimp')
emailChanged = (not existingProps) or existingProps?.email isnt doc.get('email') emailChanged = (not existingProps) or existingProps?.email isnt doc.get('email')
emailSubs = doc.get('emailSubscriptions') 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 if (not existingProps) and newGroups.length is 0
return callback?() # don't add totally unsubscribed people to the list return callback?() # don't add totally unsubscribed people to the list
subsChanged = doc.currentSubscriptions isnt JSON.stringify(emailSubs) subsChanged = doc.currentSubscriptions isnt JSON.stringify(emailSubs)
return callback?() unless emailChanged or subsChanged return callback?() unless emailChanged or subsChanged
params = {} 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.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.update_existing = true
params.double_optin = false params.double_optin = false
@ -79,18 +81,6 @@ UserSchema.pre('save', (next) ->
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.post 'save', (doc) ->
UserSchema.statics.updateMailChimp(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.username = process.env.COCO_MAIL_SERVICE_USERNAME || "";
config.mail.password = process.env.COCO_MAIL_SERVICE_PASSWORD || ""; config.mail.password = process.env.COCO_MAIL_SERVICE_PASSWORD || "";
config.mail.mailchimpAPIKey = process.env.COCO_MAILCHIMP_API_KEY || ''; 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.mail.sendwithusAPIKey = process.env.COCO_SENDWITHUS_API_KEY || '';
config.salt = process.env.COCO_SALT || 'pepper'; config.salt = process.env.COCO_SALT || 'pepper';