-- OT-MMO: crear personaje a pantalla completa (22-oct). Dos modos: -- 'cuenta' -> desde «Crear cuenta»: manda createaccountandcharacter con el email y la contraseña ya validados. -- 'personaje' -> desde «Nuevo personaje» de la lista: manda createcharacter (server/cuentas/cuentas.py valida el -- límite de 2 personajes, 5 con Premium, y vuelve a validar todo lo demás). CrearPersonaje = {} -- Habilidades = las de nivel 1 de cada vocación en gamelib/spells.lua (el ícono sale de ahí). local CLASES = { { id = 'guardian', nombre = 'Guardián', resumen = 'Aguanta golpes y protege al grupo.', roles = 'Tanque · Cuerpo a cuerpo', dificultad = 2, vida = 300, mana = 40, cap = 470, texto = 'Acero y escudo entre el grupo y lo que viene. Atrae los golpes, los aguanta y no deja que nadie de los suyos caiga. Si prefieres el hacha a dos manos, también sabe terminar peleas.', specs = { { 'Protector', 'Tanque' }, { 'Verdugo', 'Daño cuerpo a cuerpo' } }, habs = { 'Golpe Aturdidor', 'Provocación', 'Muro de Escudos' } }, { id = 'druida', nombre = 'Druida', resumen = 'Cura al grupo o trae la tormenta.', roles = 'Sanador · Apoyo · Daño', dificultad = 2, vida = 200, mana = 160, cap = 420, texto = 'Habla con la savia y la tormenta. Mantiene vivo al grupo, lo cubre con corteza o hace caer el cielo sobre el enemigo. El grupo lo busca antes que a nadie.', specs = { { 'Sanación', 'Curación' }, { 'Amparo', 'Apoyo' }, { 'Tormenta', 'Daño a distancia' } }, habs = { 'Curación Directa', 'Regeneración', 'Raíces' } }, { id = 'hechicero', nombre = 'Hechicero', resumen = 'Fuego, hielo y maná desde lejos.', roles = 'Daño mágico a distancia', dificultad = 2, vida = 180, mana = 180, cap = 400, texto = 'Fuego, hielo o maná puro. Frágil de cerca, devastador desde lejos: quema, congela o guarda el maná para una sola ráfaga que lo decide todo.', specs = { { 'Fuego', 'Daño y quemaduras' }, { 'Hielo', 'Daño y control' }, { 'Arcano', 'Ráfagas de daño' } }, habs = { 'Proyectil Arcano', 'Nova de Hielo', 'Escudo Arcano' } }, { id = 'cazador', nombre = 'Cazador', resumen = 'Arco y lobo. La más fácil.', roles = 'Distancia · Mascota', dificultad = 1, vida = 220, mana = 110, cap = 440, texto = 'Arco, flechas y un lobo que no se separa de él. Caza solo con comodidad y en grupo pone el daño desde atrás. La clase más amable para empezar.', specs = { { 'Bestias', 'Daño con bestias' }, { 'Puntería', 'Daño a distancia' } }, habs = { 'Disparo Certero', 'Lluvia de Flechas', 'Llamar al Lobo' } }, { id = 'brujo', nombre = 'Brujo', resumen = 'Roba vida y la da a sus invocaciones.', roles = 'Daño en el tiempo · Invocaciones', dificultad = 3, vida = 180, mana = 180, cap = 400, texto = 'Firmó un contrato con las manifestaciones del Pozo. Roba la vida del enemigo, la convierte en Reserva de Ceniza y la gasta en sus invocaciones o en ráfagas de fuego gris. Exigente, pero nadie aguanta tanto solo.', specs = { { 'Marchitez', 'Daño en el tiempo' }, { 'Vínculo', 'Invocaciones del Pozo' }, { 'Brasa', 'Ráfagas del Pozo' } }, habs = { 'Saeta de Ceniza', 'Beber Aliento', 'Llamar al Tizón' } }, { id = 'bardo', nombre = 'Bardo', resumen = 'Canciones que golpean, hieren o curan.', roles = 'Cuerpo a cuerpo · Distancia · Sanador', dificultad = 3, vida = 200, mana = 140, cap = 420, texto = 'Toca la guerra, la disonancia o el himno. Sus hechizos son canciones: mantén Ctrl y marca las flechas al pulso, y si caen a tiempo la canción sale Afinada. Sirve para todo, pero hay que llevar el ritmo.', specs = { { 'Canción de Guerra', 'Daño cuerpo a cuerpo' }, { 'Disonancia', 'Daño a distancia' }, { 'Himno', 'Curación' } }, habs = { 'Acorde Hiriente', 'Tonada de Ánimo', 'Marcha Ligera' } }, } -- Atuendos por clase (22-oct): Ciudadano para todos + 2 de la clase. Misma tabla que ATUENDOS_CLASE de -- server/cuentas/cuentas.py y server/scripts/apariencias.lua. El resto se desbloquea jugando. El primero es el de por defecto. local CIUDADANO = { hombre = { 128, 'Ciudadano' }, mujer = { 136, 'Ciudadana' } } local ATUENDOS_CLASE = { guardian = { hombre = { { 131, 'Caballero' }, { 134, 'Guerrero' } }, mujer = { { 139, 'Caballera' }, { 142, 'Guerrera' } } }, druida = { hombre = { { 144, 'Druida' }, { 1824, 'Monje' } }, mujer = { { 148, 'Druida' }, { 1825, 'Monja' } } }, hechicero = { hombre = { { 130, 'Mago' }, { 145, 'Hechicero arcano' } }, mujer = { { 138, 'Maga' }, { 149, 'Hechicera arcana' } } }, cazador = { hombre = { { 129, 'Cazador' }, { 143, 'Bárbaro' } }, mujer = { { 137, 'Cazadora' }, { 147, 'Bárbara' } } }, brujo = { hombre = { { 154, 'Chamán' }, { 133, 'Invocador' } }, mujer = { { 158, 'Chamana' }, { 141, 'Invocadora' } } }, bardo = { hombre = { { 273, 'Bufón' }, { 2100, 'Trovador' } }, mujer = { { 270, 'Bufona' }, { 2101, 'Trovadora' } } }, } -- Razas (25-oct, pruebas; server/scripts/razas/razas.lua). El Humano viste los de clase; Orco, Elfo y Enano, los suyos -- (los mismos para hombre y mujer). Misma tabla que RAZAS de server/cuentas/cuentas.py y Razas.DATOS de razas.lua. -- ARTE: criaturas de CipSoft, solo para las pruebas; reemplazar por arte propio antes del lanzamiento público. -- tine = el atuendo admite los 4 colores (los de criatura casi nunca). local RAZAS = { { id = 'humano', nombre = 'Humano', texto = 'Gente de Vigilia. Activa: Voluntad de Vigilia (te libra de parálisis y raíces). Pasivas: 5 % menos daño de muerte; +10 % de experiencia de Sastrería.' }, -- ponytail: Orco, Elfo y Enano retirados el 25-oct (sus atuendos no pueden montar). Ver git 9e02dbc5a para volver. } local PARTES = { { 'head', 'Cabeza' }, { 'body', 'Torso' }, { 'legs', 'Piernas' }, { 'feet', 'Pies' } } local NUM_COLORES = 133 -- paleta de atuendos de Tibia (la de game_outfit), índices 0-132 local look = { atuendo = 1, head = 78, body = 106, legs = 116, feet = 95 } -- por defecto = cuentas.py local parte = 'head' -- Suelo bajo el personaje: 3x3 baldosas de piedra oscura del juego (410), que casan con la pizarra. Los sprites de Tibia proyectan la altura -- hacia arriba a la izquierda (el atuendo 2x2 se dibuja con desplazamiento 8,8): sobre fondo liso parece inclinado -- o tumbado; con su baldosa debajo se lee de pie, como en el juego. No es un frame ni una dirección. local SUELO = 410 -- Espejo de los tokens de data/styles/0-vars.otui (Lua no lee $otmmo-*) local T = { acento = '#C1A16B', borde = '#39444C', hp = '#C94242', mana = '#347CCD' } local ventana, botones, opciones, giro local elegida, sexo, raza = CLASES[1], 'hombre', RAZAS[1] local function atuendos() if raza.atuendos then return raza.atuendos end local l = { CIUDADANO[sexo] } for _, a in ipairs(ATUENDOS_CLASE[elegida.id][sexo]) do l[#l + 1] = a end return l end local function post(cuerpo, callback) HTTP.post(Services.createAccount, json.encode(cuerpo), function(message, err) local parte = not err and message and message:match('{.*}') local ok, r = pcall(json.decode, parte or '') callback(ok and type(r) == 'table' and r or nil) end, false) end local function iconoDe(widget, nombreHabilidad) local s = SpellInfo and SpellInfo.Default and SpellInfo.Default[nombreHabilidad] if s then widget:setImageSource(SpelllistSettings.Default.iconFile) widget:setImageClip(Spells.getImageClip(s.clientId, 'Default')) end end local function apariencia() return ventana.escena.grupo.apariencia end local function pintarFigura() local a = apariencia() local at = atuendos()[look.atuendo] ventana.escena.grupo.figura:setOutfit({ type = at[1], head = look.head, body = look.body, legs = look.legs, feet = look.feet }) a.sexo.hombre:setChecked(sexo == 'hombre') a.sexo.mujer:setChecked(sexo == 'mujer') a.atuendo.nombre:setText(at[2]) for _, w in ipairs(a.razas:getChildren()) do w:setChecked(w.raza == raza) end a.razaTexto:setText(raza.texto) local tine = not raza.atuendos or at.tine -- los atuendos de criatura no se tiñen: se avisa en el título a.tituloColores:setText(tine and tr('Colores') or tr('Colores (este atuendo no se tiñe)')) for _, w in ipairs(a.partes:getChildren()) do w:setChecked(w.parte == parte) w.muestra:setBackgroundColor(getOutfitColor(look[w.parte])) end for _, w in ipairs(a.paleta:getChildren()) do w:setChecked(w.color == look[parte]) end end function CrearPersonaje.atuendo(paso) local n = #atuendos() look.atuendo = (look.atuendo - 1 + paso) % n + 1 pintarFigura() end function CrearPersonaje.parte(p) parte = p pintarFigura() end function CrearPersonaje.color(c) look[parte] = c pintarFigura() end function CrearPersonaje.azar() look.atuendo = math.random(#atuendos()) for _, p in ipairs(PARTES) do look[p[1]] = math.random(0, NUM_COLORES - 1) end pintarFigura() end function CrearPersonaje.raza(r) raza = r look.atuendo = 1 pintarFigura() end local function crearApariencia() local a = apariencia() for _, r in ipairs(RAZAS) do local w = g_ui.createWidget('Button', a.razas) w.raza = r w:setText(tr(r.nombre)) w.onClick = function() CrearPersonaje.raza(r) end end for _, p in ipairs(PARTES) do local w = g_ui.createWidget('CPParte', a.partes) w.parte = p[1] w:setText(tr(p[2])) w.onClick = function() CrearPersonaje.parte(p[1]) end end for c = 0, NUM_COLORES - 1 do local w = g_ui.createWidget('CPColor', a.paleta) w.color = c w:setBackgroundColor(getOutfitColor(c)) w.onClick = function() CrearPersonaje.color(c) end end end local function cifra(w, etiqueta, valor, color) w.etiqueta:setText(etiqueta) w.valor:setText(tostring(valor)) if color then w.valor:setColor(color) end end local function pintarClase() local c = elegida for _, b in ipairs(botones) do b:setChecked(b.clase == c) if b.clase ~= c then -- divisor de 1 px entre filas (el marco de la lista ya cierra la última) b:setBorderWidth(0) b:setBorderWidthBottom(b.ultima and 0 or 1) b:setBorderColorBottom(T.borde) end end local f = ventana.ficha f.nombre:setText(c.nombre) f.roles:setText(c.roles) f.dificultad.texto:setText(tr('Dificultad') .. ' ' .. ({ 'baja', 'media', 'alta' })[c.dificultad]) for i, barra in ipairs(f.dificultad.barras:getChildren()) do barra:setBackgroundColor(i <= c.dificultad and T.acento or T.borde) end f.texto:setText(c.texto) f.specs:destroyChildren() for _, s in ipairs(c.specs) do local w = g_ui.createWidget('CPEspecialidad', f.specs) w.n:setText(s[1]) w.r:setText(s[2]) end f.habs:destroyChildren() for _, nombre in ipairs(c.habs) do local w = g_ui.createWidget('CPHabilidad', f.habs) w.nombre:setText(nombre) w:setTooltip(nombre) iconoDe(w.marco.icono, nombre) end cifra(f.cifras.vida, tr('Vida'), c.vida, T.hp) cifra(f.cifras.mana, tr('Maná'), c.mana, T.mana) cifra(f.cifras.carga, tr('Carga'), c.cap) ventana.pie.centro.crear:setText(tr('Crear') .. ' ' .. c.nombre:lower()) end -- En pantallas anchas el contenido no pasa de ANCHO_MAX: los márgenes laterales crecen (no se estira la ficha). local ANCHO_MAX = 1440 local function ajustar() if not ventana then return end local lado = math.max(24, math.floor((ventana:getWidth() - ANCHO_MAX) / 2)) ventana.barra.volver:setMarginLeft(lado) ventana.barra.cupo:setMarginRight(lado) ventana.tituloLista:setMarginLeft(lado) ventana.ficha:setMarginRight(lado - 24) ventana.pie.centro:setMarginRight(lado) -- Figura a 4x si la escena da para suelo + apariencia lado a lado; si no, 3x (1366x768). local esc = ventana.escena local g = esc.grupo local aw = g.apariencia:getWidth() local escala = (esc:getWidth() >= 128 * 3 + 40 + aw + 48 and esc:getHeight() >= 128 * 3 + 48) and 4 or 3 local baldosa = 32 * escala g.suelo:setSize({ width = baldosa * 3, height = baldosa * 3 }) g.suelo:getLayout():setCellSize({ width = baldosa, height = baldosa }) g.figura:setSize({ width = 64 * escala, height = 64 * escala }) g.figura:setMarginRight(baldosa + 8 * escala) -- baldosa central + desplazamiento 8,8 del atuendo g.figura:setMarginBottom(baldosa + 8 * escala) g:setSize({ width = baldosa * 3 + 40 + aw, height = math.max(baldosa * 3, g.apariencia:getHeight()) }) end local function avisar(texto) if not ventana then return end local campo, etiqueta = ventana.pie.centro.nombre, ventana.pie.centro.error campo:setOn(texto ~= nil) etiqueta:setText(texto or '') end function CrearPersonaje.sexo(s) sexo = s pintarFigura() end function CrearPersonaje.sugerir() post({ type = 'generatecharactername' }, function(r) if ventana and r and r.GeneratedName then ventana.pie.centro.nombre:setText(r.GeneratedName) avisar(nil) end end) end -- opts = { modo = 'cuenta'|'personaje', email, password, cupo = 'Personaje 2 de 2', alCrear = fn(), alVolver = fn() } function CrearPersonaje.abrir(opts) CrearPersonaje.cerrar() opciones = opts look = { atuendo = 1, head = 78, body = 106, legs = 116, feet = 95 } parte = 'head' raza = RAZAS[1] ventana = g_ui.displayUI('crearPersonaje') ventana.barra.titulo:setText(opts.modo == 'cuenta' and tr('Tu primer personaje') or tr('Nuevo personaje')) ventana.barra.cupo:setText(opts.cupo or '') botones = {} for _, c in ipairs(CLASES) do local b = g_ui.createWidget('CPClase', ventana.lista) b.clase = c iconoDe(b.icono, c.habs[1]) b.nombre:setText(c.nombre) b.resumen:setText(c.resumen) b.onClick = function() elegida = c look.atuendo = math.min(look.atuendo, #atuendos()) pintarClase() pintarFigura() end table.insert(botones, b) end botones[#botones].ultima = true -- el marco de la lista ya dibuja su borde de abajo for _ = 1, 9 do g_ui.createWidget('CPBaldosa', ventana.escena.grupo.suelo):setItemId(SUELO) end crearApariencia() ventana.pie.centro.nombre.onTextChange = function() avisar(nil) end ventana.onGeometryChange = ajustar ajustar() pintarClase() pintarFigura() -- Gira al personaje como en la pantalla de WoW: una dirección cada 2 s. local dir = 2 giro = cycleEvent(function() if ventana then dir = (dir + 1) % 4 ventana.escena.grupo.figura:setDirection(dir) end end, 2000) ventana.pie.centro.nombre:focus() CrearPersonaje.sugerir() end function CrearPersonaje.cerrar() if giro then removeEvent(giro) giro = nil end if ventana then ventana:destroy() ventana = nil end end function CrearPersonaje.volver() local f = opciones and opciones.alVolver CrearPersonaje.cerrar() if f then f() end end function CrearPersonaje.crear() local o = opciones local boton = ventana.pie.centro.crear local nombre = ventana.pie.centro.nombre:getText():trim() boton:disable() post({ type = o.modo == 'cuenta' and 'createaccountandcharacter' or 'createcharacter', EMail = o.email, Password = o.password, CharacterName = nombre, CharacterSex = sexo, CharacterVocation = elegida.id, CharacterRace = raza.id, LookType = atuendos()[look.atuendo][1], LookHead = look.head, LookBody = look.body, LookLegs = look.legs, LookFeet = look.feet, }, function(r) if not ventana then return end boton:enable() if not r then avisar(tr('No se pudo contactar al servidor. Intenta de nuevo.')) elseif not r.Success then avisar(r.errorMessage or tr('No se pudo crear el personaje.')) else local f = o.alCrear CrearPersonaje.cerrar() if f then f() end end end) end