Jump to content
Sign in to follow this  
caanmasu

[TUTORIAL QUEST] Cómo hice el editor de ítems

Recommended Posts

Hola a todos

 

En esta ocasión voy a explicarles con pleno detalle cómo hice el editor de ítems. Aclaro que la versión que tengo posteada es antigua en cuanto a código, y la versión que les voy a enseñar ahora es basada en la experiencia que he adquirido luego. Esto quiere decir, código limpio, modular, más desacoplado, más cohesivo, mejores nombres de variables, todo en inglés, mejor estructurado, más dinámico, más fácil de leer y modificar, más actualizado en cuanto a tipos de objetos y bonus, convertido en librería, mejor dicho, toda una obra de arte.

Verán la forma de programación más profesional que jamás hayan visto, y lo mejor de todo, esto es gratis. Esta quest no fue hecha una vez en tres días, sino fue la experiencia de una persona que lleva más de 5000 horas de programación en lua y quests, con una carrera profesional en este tipo de temas y varios años de conocimiento sobre el juego y su software.

 

Esto va a ser extenso pero a la vez va a estar muy enriquecido.

 

Les cuento que la primera vez que la empecé a hacer, no sabía cómo hacerla, solo empecé. ¿Alguna vez les ha pasado? lo importante es escribir la primera línea de código, nunca te quedes pensando cómo hacer algo o nunca lo harás; este es mi primer consejo.

 

Cuando empecé a hacerla, ya había creado el bloque quest, state y empezaba mi primer when, fue el npc donde iba a hacer el take. Con mi experiencia en quest pude saber hacia dónde me tenía que dirigir luego.

 

Aquí vamos.

Primero que todo debemos saber cómo es que se supone que debería funcionar. Obviamente ustedes ya la vieron y la metieron en su sv. Pero pónganse en mis zapatos, que yo solo tenía una idea en la cabeza, no tenía ninguna quest para guiarme. ¿Cómo es que se hace para llevar una idea a que se haga real?

Lo único que pensé fue, bueno, quiero que algo agarre cierto objeto equipable que se le pueda meter bonus, me muestre los bonus y valores que pueda elegir y ya, cuando mire el objeto ya tenga esos bonus puestos tal cual el orden en que los elegí.

Esto lo vi posible porque sé el alcance que tienen las quests, aunque sean limitadas.

 

En este punto sabía que un ítem se puede seleccionar cuando "algo" agarre el id de él (ojo, el id, no el vnum).

La diferencia entre el id de un objeto y el vnum, es que el id representa la identidad de un objeto, y ese id es único en cada objeto. Mientras que el vnum es la "plantilla" de ese objeto, de manera que cada vez que crees un objeto con el vnum, estarás sacando el objeto de la plantilla de los ítems que es el item_proto. Proto significa base. Los objetos ya creados aparecen en la tabla player.item.

 

Sigo...

Una vez un NPC captura el id de un objeto con el disparador take (los login, logout, take, .chat, unmount, etc son triggers pero traducidos son disparadores), ya puedo modificar todos sus atributos que están en las columnas de la tabla player.item.

Con la tabla player.item_proto puedo saber si el objeto es un arma o una armadura, y si tiene media o no; ya que ese atributo está desde la base.

Para saber esto debes haber estudiado muy bien el item_proto ya que es lo más básico en el desarrollo de servidores.

También en la tabla player.item_proto puedo ver los bonus base que el objeto ya tiene, para no tener que repetirlos.

Otra cosa de las quests, es que tienen unas funciones listadas en quest/quest_functions. Ahí hay muchas que son útiles para modificar atributos del ítem y lo hace desde el source, o sea que la modificación se hace de inmediato. Las vamos a ver más adelante.

Luego necesito mostrar los bonus que están disponibles para ese objeto, que son los mismos bonus que me salen cuando cambio los bonus. Estos bonus están en player.item_attr. Esto también es básico cuando estás aprendiendo estos temas.

Aquí se complica un poco las cosas... pero, les digo de una vez que no pensé en nada de eso cuando empecé a hacer la quest.

 

 

Iniciemos

 

Esta quest será modular, es decir, utilizaré funciones que se conectarán las unas con las otras para lograr nuestro resultado. Es la mejor manera que he encontrado para que cualquier otra persona que vea la quest pueda entenderla muy rápido.

 

item_editor_camilo.quest

quest item_editor_camilo begin
	state start begin
		when 11000.take or 11002.take or 11004.take begin
			item_editor_camilo_lib.startItemEditor()
		end
	end
end

Si no entiendes lo de los bloques o no tienes un entorno preparado para programar quests, lee y haz los pasos de este post: 

"cuando" 11000 (guardián de la ciudad) "tome" "empieza"
Ahí estamos diciendo literalmente que cuando el NPC de vnum 11000, 11002 o 11004 tome un objeto (es arrastrado hacia él), entonces empiece a ejecutar lo que viene abajo y "termine" en donde dice end.

item_editor_camilo_lib.startItemEditor()

En esta instrucción estamos llamando a una función de una tabla que no hemos creado. Vamos a crearla.

 

item_editor_camilo_lib.lua

item_editor_camilo_lib = {}

item_editor_camilo_lib.startItemEditor = function()
	chat("Esto debería mostrar cuando ponga un objeto.")
end

Para esto crearás un archivo llamado item_editor_camilo_lib.lua. En verdad el nombre no importa pero la extensión sí, no la cambies, tiene que ser lua.

Un archivo es .quest cuando el código empieza por quest [nombre] begin.

Un archivo es .lua cuando no empiece por quest. En este caso no empieza por quest.

Este archivo por el momento no tiene conexión con nada. Nuestro archivo padre se llama questlib.lua que ya está configurado desde la fuente para que sea la librería papá. Allí se pone todo tipo de código suelto, funciones, inicializar variables, llamar a otros archivos, etc.

Conectaremos el questlib.lua con nuestra librería que acabamos de crear. Para ello ponemos esta línea en el questlib.lua:

dofile(get_locale_base_path().."/quest/system/_item_editor_camilo/item_editor_camilo_lib.lua")

Esta instrucción si la traduces al español lo dice todo pero igual te lo voy a explicar. Aquí el questlib.lua se extiende hacia ese archivo que declaras en esa ruta, por lo tanto, lo ejecuta y lo tendrá en cuenta cuando llamen a alguna función o variable que esté declarada allí.

Ese get_locale_base_path() devuelve locale/xxxx. En xxxx va el country o país de tus files, sean turkey, germany, spanish, etc. Ese get_locale_base_path() no lo toques, solo modifica lo que está en verde. Y si no te funciona así, pon la ruta completa desde el /usr.

Lo más probable es que la ruta que tengas no sea la misma mía pero sí te recomiendo que sigas el mismo patrón que las otras quests. Por ejemplo, si en tu carpeta quest hay una carpeta system o source donde meten todas las quests, métela ahí. Si en tu carpeta quest ponen las quest en carpetas que empiezan en "_", crea una carpeta nueva con el mismo patrón.

Te recomiendo crear una carpeta donde estará el editor, no combines esta quest y librería con varios archivos de otras cosas.

 

En ese archivo lib que creamos he hecho las funciones de una de las maneras; hay otra manera, como están en questlib.lua pero nosotros somos más sofisticados y las hemos guardado en una tabla.

 

Prueba la quest y revisa si en el juego te funciona para ver si todo está bien.

Si te funciona, sigamos.

 

Vamos a hacer que el NPC determine si el objeto tiene media y habilidad. La verdad no nos importa si es arma, armadura, acc, etc.

item_editor_camilo_lib.startItemEditor = function()
	local _, item_table = mysql_direct_query(string.format("SELECT addon_type FROM player.item_proto WHERE vnum = %s;", item.vnum))
	if tonumber(item_table[1].addon_type) == -1 then
		--aquí haríamos lo que haríamos si el objeto tuviera media
	else
		syschat("Este objeto no se puede editar.")
	end
end

Un momento... ¿Qué acabo de hacer?

Bueno, primero que todo vamos a tener que usar la maravillosa función de mysql_direct_query que les dejaré como adjunto al final de este post para que la metan en el src.

La saqué de 

Debes iniciar sesión para ver el contenido del enlace en esta publicación.

Solo hay que meterla y ya, ya les explicaré cómo funciona.

Uso del string.format: resulta que cuando creamos los chat o los say o derivados textos que se muestran en el personaje, ponemos variables entre esos textos y no tienen un formato fácil de leer.

A mí me gusta mucho usar el string.format.

Sin string.format:

chat("Soy "..pc.get_name().." y soy nivel "..pc.level)
--Esto muestra:
--Soy Camilo y soy nivel 120

Con string.format:

chat(string.format("Soy %s y soy nivel %s", pc.get_name(), pc.level))
--Esto muestra:
--Soy Camilo y soy nivel 120

Son dos formas de crear un string (cadena de texto) pero una es más fácil de leer y modificar que la otra. Ese %s es el que determina la variable, y esa "s" se refiere a que es string. La verdad hay un %d que se refiere a double (números decimales) pero no la uso porque finalmente así sea número va a mezclarse con un string y todo será un string. Así que para todas será siempre %s.

Por si te enredas con ese código, míralo de esta manera:

local texto_presentacion = string.format("Soy %s y soy nivel %s", pc.get_name(), pc.level)
chat(texto_presentacion)
--Esto muestra:
--Soy Camilo y soy nivel 120

Así se entiende mejor, ¿verdad?

Mira ese item.vnum, te preguntarás que por qué item.vnum y no item.get_vnum(). La respuesta está en el questlib.lua.

Hay tablas donde asignan nuevos valores a estas funciones, miren:

image.pngAsí que si pones por ejemplo item.type, será lo mismo que poner item.get_type().

Por qué funciona eso así? por unas estructuras llamadas metatables.

image.pngLo mismo funciona para algunas funciones de pc y npc. Solo debes revisar.

Ahora vamos por entender la query.

Query significa consulta, refiriéndonos a la base de datos. Una "búsqueda" en la base de datos.

Hay un lenguaje llamado SQL que también se estudia y todo, pero es más básico que los lenguajes que conocemos.

SELECT addon_type FROM player.item_proto WHERE vnum = %s;

Aquí le estoy diciendo en español "seleccione" addon_type [columna] "de" player.item_proto[tabla] "donde" vnum = xxxx;

No es el objetivo enseñar SQL porque hay tutoriales en internet pero haré lo posible para explicar de pasada y quede algo en la mente.

Ese player.item_proto lo pongo porque necesito seleccionar una base de datos. Recuerden que las bases de datos son account, common, information_schema, log, mysql, etc. Es decir, las que aparecen debajo de tu conexión.

image.pngEl mismo ícono indica una base de datos.

La forma sería base_de_datos.nombre_tabla

El resultado de esta consulta obviamente será un solo registro o resultado porque solo hay un vnum único en item_proto.

image.png

image.png

 

Ya sabemos el tipo de resultado que puede arrojar esa consulta. Ahora si el número el diferente de -1 pues deducimos que no es un arma de media y listo.

local _, item_table = mysql_direct_query(...

¿Qué significa ese "_" y por qué hay dos variables que se asignan luego de usar la función mysql_direct_query?

Si ven la función por source, verán que retorna dos variables.

Dada la estructura de la tabla que retorna, no se puede obtener la cantidad de registros usando el típico table.getn().

La primera variable es la cantidad de registros obtenidos de la consulta, y la segunda variable es la tabla obtenida de la consulta.

Puse un "_" porque no voy a utilizar la cantidad de registros en ninguna parte. Es obligatorio poner las dos variables o sino habría un problema de redundancia.

La estructura de las tablas obtenidas de mysql_direct_query es:

nombre_tabla[num_registro][nombre_columna]

Las típicas funciones de mysql_query tienen otra estructura.

Entonces para obtener el -1 debemos hacer esto:

item_table[1].addon_type
--o que es lo mismo:
item_table[1]['addon_type']

Explicación de esa forma de acceder al dato:

Resulta que si tienes una variable que no tiene espacios puedes hacer como en la primera opción.

Pero si tienes una variable con espacios, no puedes hacer esto:

item_table[1].addon type --error

En cambio sí puedes hacer esto:

item_table[1]['addon type'] --aunque obviamente en las columnas de la db no pueden haber espacios

Aclaro que solo en dado caso de que la variable tenga espacios o uses variables dentro de ella, que ya lo veremos más adelante.

Cuando recibimos ese -1 puede ser un -1 propiamente, o puede ser un "-1". Me ha pasado de las dos formas. Así que es mejor asegurarnos.

Hay que poner el tonumber() así como está en el código, para garantizar que estamos comparando con un número.

No es lo mismo "-1" que -1. El primero es un string y el segundo es un number, no se pueden comparar.

Como opcional ponemos que en dado caso que no sea un objeto con media o habilidad, le decimos que no se puede editar el objeto.

 

Pasemos a la siguiente función:

 

item_editor_camilo_lib.startItemEditor = function()
	local _, item_table = mysql_direct_query(string.format("SELECT addon_type FROM player.item_proto WHERE vnum = %s;", item.vnum))
	if tonumber(item_table[1].addon_type) == -1 then
		--aquí haríamos lo que haríamos si el objeto tuviera media
    		item_editor_camilo_lib.switchingMeanSkill()
	else
		syschat("Este objeto no se puede editar.")
	end
end

Para llamar a otra función hay que seleccionar la tabla y luego llamar a la función con el punto.

item_editor_camilo_lib.switchingMeanSkill()

switchingMeanSkill significa cambiar media y habilidad.

 

Vamos a crearla:

item_editor_camilo_lib.switchingMeanSkill = function()
	say_title_center("Editor de ítems By Camilo[ENTER]")
	say_item_vnum(item.vnum)
	say("Ingresa la media que quieres sacar")
	local mean_chosen = tonumber(input())
	if mean_chosen == nil or mean_chosen < 0 or math.mod(mean_chosen, 1) != 0 then
		return
	end
	say_title_center("Editor de ítems By Camilo[ENTER]")
	say_item_vnum(item.vnum)
	say("Ingresa la habilidad que quieres sacar")
	local skill_chosen = tonumber(input())
	if skill_chosen == nil or skill_chosen < 0 or math.mod(skill_chosen, 1) != 0 then
		return
	end
end

Le vamos a mostrar al personaje el menú cuando le pongamos el objeto de media.

Decoramos con say_title_center. Si no tienes esa función, dejo todo adjunto al final.

También decoramos con say_item_vnum poniendo el ícono del objeto en el menú.

Acá le juego a un método: la persona selecciona un valor de media y un valor de habilidad, y el primero que caiga mayor o igual, se queda.

Voy a explicar esto muy bien porque vi que muchas personas no lo entendieron.

Si yo pongo media 50 y habilidad 25, cuando caiga 25 de habilidad se va a detener ahí. Si cae 26 o 27 de habilidad también se detiene ahí.

Si cae 52 de media primero que 25 de habilidad, pues quedan esos 52 de media y se detiene el dopador.

Nota: en la parte de los bonus de la media y habilidad es un dopador, no es un editor, ya que estamos haciendo cambios realmente.

Le pediremos a la persona que digite un número. Esto se hace con input() y lo convertimos a número con tonumber().

Hacemos la validación. La validación es un filtro. Si el texto entregado por la persona no nos sirve en nuestras operaciones, no debemos dejarlo pasar.

Si la persona dejó en blanco el espacio ponemos:

if mean_chosen == nil

Si la persona puso media negativa tampoco lo dejemos pasar.

Y si la persona puso números decimales tampoco lo dejemos pasar; yo lo hago con math.mod, explico:

Hay una operación matemática que hace parte de la aritmética que se llama módulo (representado en programación por "%") donde el resultado es el residuo de la división de dos números.

Sabemos que un número par que se divide entre 2, el resultado será un número entero. Esto se llama ser divisible.

Si el 7 se divide entre 3 da un número decimal. Entonces el 7 y el 3 no son divisibles, o sea que el residuo no es 0.

Si yo tomo un número y lo divido entre 1, siempre dará el mismo número.

Si el número 2 lo divido entre 1 me da 2. El residuo es 0 porque el número es entero.

Si el número 2.1 lo divido entre 1 me da 2.1. El residuo no es 0 ya que es decimal.

Esto quiere decir que si un número dividido entre 1 me da decimal, el residual no es 0.

No había otra manera sencilla de decirle a lua si un número es decimal porque no existe una función "is_integer" o "es_entero" o algo así.

Entonces se me ocurrió poner:

math.mod(numero, 1) != 0

para definir si el número es decimal.

 

Regla importante sobre el return:

Si el return está en una función, no se terminará la quest ahí. Solo devolvería algo.

Si llegáramos a poner sentencias debajo de nuestro bloque if:

item_editor_camilo_lib.startItemEditor = function()
	local _, item_table = mysql_direct_query(string.format("SELECT addon_type FROM player.item_proto WHERE vnum = %s;", item.vnum))
	if tonumber(item_table[1].addon_type) == -1 then
    		item_editor_camilo_lib.switchingMeanSkill()
	else
		syschat("Este objeto no se puede editar.")
	end
	--lo que esté de aquí para abajo se ejecutaría
end

y también:

quest item_editor_camilo begin
	state start begin
		when 11000.take or 11002.take or 11004.take begin
			item_editor_camilo_lib.startItemEditor()
			--se ejecutaría de aquí para bajo si hubiese algo...
		end
	end
end

Así que pensemos esto: si retornamos en una función, se devolverá un dato. Si retornamos en un bloque when, se detendrá la quest.

 

Ahora vamos a meter la segunda parte de nuestra función:

 

item_editor_camilo_lib.switchingMeanSkill = function()
	say_title_center("Editor de ítems By Camilo[ENTER]")
	say_item_vnum(item.vnum)
	say("Ingresa la media que quieres sacar")
	local mean_chosen = tonumber(input())
	if mean_chosen == nil or mean_chosen < 0 or math.mod(mean_chosen, 1) != 0 then
		return
	end
	say_title_center("Editor de ítems By Camilo[ENTER]")
	say_item_vnum(item.vnum)
	say("Ingresa la habilidad que quieres sacar")
	local skill_chosen = tonumber(input())
	if skill_chosen == nil or skill_chosen < 0 or math.mod(skill_chosen, 1) != 0 then
		return
	end
	--Lo nuevo agregado de aquí para abajo
  	local switch_max_amount = 1000000 
	local mean_calculated, skill_calculated, switchs = get_mean_skill(mean_chosen, skill_chosen, switch_max_amount)
	
	if switchs <= switch_max_amount then
		if mean_calculated == 0 then
			item.set_value(0, 71, skill_calculated)
			item_editor_camilo_lib.addAttr(4)
		elseif skill_calculated == 0 then
			item.set_value(0, 72, mean_calculated)
			item_editor_camilo_lib.addAttr(4)
		else
			item.set_value(0, 72, mean_calculated)
			item.set_value(1, 71, skill_calculated)
			item_editor_camilo_lib.addAttr(3)
		end
	else
		syschat("No dopó. Inténtalo de nuevo")
	end
end

La función get_mean_skill es de source y la he creado a partir del source game en item_addon.cpp. Ese archivo define la distribución de la media y la habilidad y devuelve la media y habilidad que haya salido.

En los archivos adjuntos estará la función get_mean_skill que debes agregarla al source game en questlua_global.cpp.

Toda función que no tenga prefijo o no haga parte de una agrupación debe ir en questlua_global. Si fuera pc.get algo, iría en questlua_pc.cpp. Si fuera item.set algo, iría en questlua_item.cpp. Entiendes el patrón?

Nota: debes fijarte cómo se declara la función, si es con ALUA o con int; siempre sigue el patrón...

Aparte de agregarla en source game, debes agregar el nombre en quest_functions.

 

Voy a mostrarles un poco la función. No hay que programar nada, solo explico un poco:

 

Parte de questlua_global.cpp

	ALUA(_get_mean_skill)
	{
		if (!lua_isnumber(L, 1) || !lua_isnumber(L, 2) || !lua_isnumber(L, 3))
		{
			return 0;
		}
		int mean = (int)lua_tonumber(L, 1);
		int skill = (int)lua_tonumber(L, 2);
		int switch_max = (int)lua_tonumber(L, 3);
		
		int iSkillBonus = 0;
		int iNormalHitBonus = 0;
		int switching = 0;
		
		for (int i = 0; i < switch_max; i++)
		{
			iSkillBonus = MINMAX(-30, (int)(gauss_random(0, 5) + 0.5f), 30);
			iNormalHitBonus = 0;
			if (abs(iSkillBonus) <= 20)
				iNormalHitBonus = -2 * iSkillBonus + abs(number(-8, 8) + number(-8, 8)) + number(1, 4);
			else
				iNormalHitBonus = -2 * iSkillBonus + number(1, 5);
			
			switching = i+1;
			if (iNormalHitBonus >= mean || iSkillBonus >= skill)
				break;
		}
		
		lua_pushnumber(L, iNormalHitBonus);
		lua_pushnumber(L, iSkillBonus);
		lua_pushnumber(L, switching);
		
		return 3;
	}

Si no ponemos un límite en cambios, se iría a ciclo infinito y se paraliza el juego si una persona pone una media inexistente, por ejemplo 61 o mayor.

switch_max es el máximo de cambios que le pondremos a la función. Es el 3° parámetro de la función.

La función recibe tres parámetros, la media mínima que debe salir, la habilidad mínima que debe salir y la cantidad máxima de cambios que debe hacer el ciclo.

Lo que está dentro del ciclo es muy parecido a la función de addon_type.cpp.

La diferencia es que cuento cada cambio y rompo el ciclo cuando me salga la media o habilidad.

El retorno se hace así como lo puse allí, con los lua_push y la cantidad de valores retornados los debo poner en al final, en este caso return 3.

Devuelvo la media que salió, la habilidad que salió y la cantidad de veces que se hizo. La cantidad de veces serían los cambios.

 

Recuerden que hay una línea que se debe agregar en ese mismo archivo para que pueda funcionar. Esto está en el archivo adjunto.

 

Muchos me preguntan que cuál es la máxima media y la máxima habilidad.

Esta función se probó millones de veces; la media nunca pasó de 60, y la habilidad nunca pasó de 27.

 

En la tercera variable puse switchs, esos son los cambios que se hicieron. Ahora le digo al sistema que si son menores o iguales a la cantidad de cambios máximos, entonces siga. Pero si no, que diga que no dopó.

Como máxima cantidad de cambios que hará el source pongo 1000000, es un buen valor. ¿Qué sucede si pongo 10 millones, por ejemplo? El servidor mientras realiza ese proceso le dará prioridad y el juego se quedará quieto. En cambio si pongo 1000000 demorará milisegundos y pasará desapercibido.

Variables globales vs locales:

Las variables locales mueren cuando se termina un bloque en la jerarquía del when o function. Mueren quiero decir que se vacían, o sea que no se podrán usar luego en otros bloques de esa jerarquía.

Las variables globales reservan los datos y cada vez que haces rel q se cargan de nuevo. Si vas a usar una variable global te debes asegurar de que no se interfiera con otra con el mismo nombre.

Cuando uses variables locales asegúrate que solo las vayas a usar en ese bloque. No dejes variables con valores almacenados y no los vayas a usar, no llenes el sv de basura.

Las variables globales se crean normal, solo no hay que poner el "local".

Pondré el max_switch_amount como variable global en el archivo adjunto para que puedan ver.

Ahora tengo que validar si la media es 0 o la habilidad es 0 porque en los bonus no se muestra cuando un bonus tiene valor 0. Esta parte es fácil, si alguno de los dos es 0, entonces los bonus que agrego sería 4.

Si los dos bonus son diferentes de 0, entonces agrego 3 bonus más.

Esto es para que me queden los 5 bonus.

 

La parte del item.set_value. Esta función es de source game. Aparece en questlua_item.cpp. Funciona así:

item.set_value(num_bonus, id_bonus, valor_bonus)

Al ítem seleccionado (sabemos que el NPC lo seleccionó y cada vez que hagas item. algo, hará algo con ese ítem) se le asignará un bonus.

num_bonus me refiero a los números entre 0 y 4 de los bonus. Siendo 0 el primer bonus, me refiero al orden.

id_bonus es el bonus número de bonus que aparece declarado en source game en constants.cpp. Si vas allí, busca:

const TApplyInfo aApplyInfo[MAX_APPLY_NUM] =

En esa tabla aparecerán los nombres de los bonus, antecedidos por POINT_, y en comentario aparece el id.

Para el 71 es SKILL_DAMAGE_BONUS.

Para el 72 es NORMAL_HIT_DAMAGE_BONUS.

En el questlib.lua aparecen los bonus pero están incompletos porque hace años no se toca la tabla apply. Por eso en los archivos adjuntos voy a agregar los demás bonus que salen en los objetos.

item_editor_camilo_lib.addAttr(4)

Esta función sería la que agregaría esos siguientes bonus.

 

Para que no se vea abrumador, no pondré la función completa. La pondré por partes.

item_editor_camilo_lib.addAttr = function(bonus_count)
	available_attr = item_editor_camilo_lib.getAvailableAttr()
end

Esta nueva función es la de agregar bonus. Ese Attr significa Attribute o atributos.

El parámetro que recibe se llama bonus_count y es el que nos va a decir cuántos bonus se le van a agregar al ítem.

Para agregar esos bonus primero necesitamos saber cuáles son los bonus que podemos agregar al objeto porque no podemos meter HP en una arma, por ejemplo.

Creamos la función getAvailableAttr()

item_editor_camilo_lib.getAvailableAttr = function()
	local id_types_base = item_editor_camilo_lib.getApplyTypeBase()
	local r, item_attr_table = mysql_direct_query(string.format("SELECT apply as name_apply, prob, lv1, lv2, lv3, lv4, lv5 FROM player.item_attr WHERE %s > 0;", item_editor_camilo_lib.getNameSubtype(item.type, item.sub_type)))
	local available_attr = {}
	for i = 1, r do
		local name_apply = item_attr_table[i].name_apply
		local id_apply = apply[name_apply]
		if not item_editor_camilo_lib.isAttrIncluded(id_apply, id_types_base) then
			table.insert(available_attr, {})
			local current_pos = table.getn(available_attr)
			available_attr[current_pos][0] = id_apply
			available_attr[current_pos][1] = name_apply
			available_attr[current_pos][2] = apply_human[id_apply][2]
			available_attr[current_pos][3] = {}
			for j = 1, 5 do
				available_attr[current_pos][3][j] = item_attr_table[i]["lv"..j]
			end
			available_attr[current_pos][4] = item_attr_table[i].prob
		end
	end
	return available_attr
end

Esto se ve complejo pero vamos a desglosarlo.

Necesitamos quitar de los bonus disponibles (aquellos que tienen un 5 en la tabla player.item_attr en la db según el tipo de ítem) los que ya tiene de base el ítem.

Si el armadura tiene HP de base, no dejaremos que salga HP en bonus, por ejemplo.

La idea es guardar esos types de bonus (o sea, el id) en una tabla para luego comparar y quitarlos de la tabla de disponibles.

Creamos la función:

item_editor_camilo_lib.getApplyTypeBase = function()
	local _, apply_type_base = mysql_direct_query(string.format("SELECT applytype0, applytype1, applytype2 FROM player.item_proto WHERE vnum = %s;", tonumber(item.vnum)))
	local id_types_base = {}
	for i = 0, 2 do
		if tonumber(apply_type_base[1]["applytype"..i]) > 0 then
			table.insert(id_types_base, apply_type_base[1]["applytype"..i])
		end
	end
	return id_types_base
end

La query lo que hace es recoger los type que están en player.item_proto de ese objeto. No hay nada raro ahí.

Les mostraré en el caso de un Brazalete de plata +9 para que el ejemplo sirva. Ya que este brazalete tiene HP de base y en bonus también tiene disponible el HP.

image.png

image.png

La tabla arroja el type 7 que es velocidad de ataque y el 1 que es HP. Recuerda revisar constants.cpp para ver los ids o types de los bonus.

Ahora viene la parte de meter una variable en la tabla que llegó de la query.

No podía poner esto:

apply_type_base[1].applytype..i

Aunque pensarás, bueno, podríamos hacer esto:

apply_type_base[1].applytype0
apply_type_base[1].applytype1
apply_type_base[1].applytype2

Pero tendríamos que hacer tres if evaluando si el número es mayor a 0 porque sería inútil llenar la tabla de ceros.

Finalmente nos queda:

apply_type_base[1]["applytype"..i]

Lo demás ya lo expliqué más arriba.

Bien, para insertar en una tabla debemos poner table.insert(nombre_tabla, valor_para_meter)
Cuando insertamos en una tabla, se agrega un nuevo valor en la tabla, o sea, si está vacía, se agregará el valor en el primer índice.

En lua los índices empiezan en 1.

Si hacemos 3 inserts, la tabla queda así:

[1] = valor1,
[2] = valor2,
[3] = valor3

Retornamos la tabla. La idea de retornar las tablas es que se puedan reutilizar en otras ocasiones. Ya si quisieras crear otra quest donde necesites obtener los bonus base del equipamento, solo llamas a la función y ya.

local r, item_attr_table = mysql_direct_query(string.format("SELECT apply as name_apply, lv1, lv2, lv3, lv4, lv5 FROM player.item_attr WHERE %s > 0;", item_editor_camilo_lib.getNameSubtype(item.type, item.sub_type)))

Vamos por esta parte:

Esta query es la tabla player.item_attr pero lo que nos importa es sacar los bonus de cierto tipo de equipo. Es decir, arma, armadura, ciertos acc, etc.

Revisa la tabla player.item_proto, vas a ver que hay columnas que representan el tipo de equipamento, y en esa columna donde esté el 5 es porque admite el respectivo bonus.

image.png

Ejemplo para armas, aquí quiere decir que en las armas sale CON (vit), INT, STR, DEX, CAST_SPEED (velocidad de hechizo) y así...

La prob no la tendremos en cuenta que que este es un editor, no un dopador.

Sobre los lvs:

En el source game item_attribute.cpp está la lógica de cómo funciona esto, pero no lo voy a explicar ahora porque esto es un editor, solo mostraremos los 5 valores y el que se seleccione, ese queda.

En la query le estamos diciendo al sistema que seleccione la columna apply. ¿Qué es ese "as"? "as" significa "como". Seleccione "apply" como "name_apply", es decir, le estoy cambiando el nombre. ¿Por qué hago esto? porque si no lo hago, habrá redundancia con otro apply (una tabla) que está en el questlib que utilizaremos ahorita.

Lo explico mejor:

Ya sabemos que hay una columna llamada apply, que termina siendo una tabla aquí en lua.

También sabemos que hay una tabla llamada apply que está en questlib.lua y que contiene los nombres de los bonus con sus ids.

Al llamar a apply, el sistema a cuál de los dos tomará? ...

Eso se llama redundancia. Seguro el apply nuevo de la query reemplaza al que ya teníamos, y perderíamos los datos.

Ahora sí, en la query le decimos al sistema que seleccione apply con el nuevo nombre de name_apply, además seleccione la columna lv1, lv2... lv5 de la tabla player.item_attr donde [tipo de equipamento] sea mayor a 0.

Cuando obtengamos la tabla de esa consulta, la columna no será apply sino name_apply.

Al ejecutar la query, poniendo en %s weapon, obtenemos:

image.png

image.png

Ahora se va viendo la forma.

En el %s va el tipo de equipamento. Revisa en player.item_attr las columnas.

En mi caso tengo weapon (arma), wrist (brazalete), foots (zapatos), neck (collar), head (casco), shield (escudo), ear (pendientes), pendant (talismán) y glove (guantes).

Vayamos a esta parte de esa línea:

item_editor_camilo_lib.getNameSubtype(item.type, item.sub_type)

Aquí debemos crear una función donde obtengamos ese tipo de equipamento. Ya sabemos que lo obtendremos dando el type y el subtype.

Si es un arma, el type es 1. Si es cualquier otro equipamento el type es 2. Dentro del type 2 hay varios subtypes que determina si es brazalete, zapatos, casco, etc.

item_editor_camilo_lib.getNameSubtype = function(attr_type, attr_subtype)
	if attr_type == 1 then
		return "weapon"
	elseif attr_type == 2 then
		local name_subtypes = {
			[0] = "body",
			[1] = "head",
			[2] = "shield",
			[3] = "wrist",
			[4] = "foots",
			[5] = "neck",
			[6] = "ear",
			[7] = "pendant",
			[8] = "glove"
		}
		return name_subtypes[attr_subtype]
	end
end

Esta estructura hasta el día de hoy es la más eficiente para mí.

Diferencia entre hacer varios if y hacer if, elseif, else:

Si pongo varios if, se ejecutarán todos.

Si pongo un if con varios elseif, se ejecutará solo el que cumpla, en orden.

Por lo tanto, el programa haría menos instrucciones si lo haces con elseif, para este caso.

Ejemplo con solo if:

local level = pc.level
if level > 15 then
  chat("Soy mayor que 15")
end
if level > 30 then
  chat("Soy mayor que 30")
end
if level > 50 then
  chat("Soy mayor que 50")
end

Va a mostrar:

Soy mayor que 15
Soy mayor que 30
Soy mayor que 50

Dado que es la misma variable, conviene usar if, elseif, end para que muestre solo un resultado, así:

local level = pc.level
if level > 15 then
  chat("Soy mayor que 15")
elseif level > 30 then
  chat("Soy mayor que 30")
elseif level > 50 then
  chat("Soy mayor que 50")
end

Si eres nivel 51, de todas maneras te dirá

Soy mayor que 15

pero en este caso de niveles es mejor hacerlo desde el más alto hasta el más bajo:

local level = pc.level
if level > 50 then
  chat("Soy mayor que 50")
elseif level > 30 then
  chat("Soy mayor que 30")
elseif level > 15 then
  chat("Soy mayor que 15")
end
Soy mayor que 50

Ahora sí.

Aunque en nuestro caso da igual porque retornamos directamente el valor en cada if o elseif, pero se ve mejor programado.

Hay veces que es necesario utilizar la estructura [clave] = valor como en este caso con los subtypes.

local name_subtypes = {
	[0] = "body",
	[1] = "head",
	[2] = "shield",
	[3] = "wrist",
	[4] = "foots",
	[5] = "neck",
	[6] = "ear",
	[7] = "pendant",
	[8] = "glove"
}

Esto es mejor que hacer esto:

local name_subtypes = {
	{0, "body"},
	{1, "head"},
    	{2, "shield"},
  	{3, "wrist"},
  	{4, "foots"},
  	{5, "neck"},
  	{6, "ear"},
  	{7, "pendant"},
  	{8, "glove"}
}

¿Por qué? porque para obtener el nombre del subtipo es mejor hacerlo en una línea que hacerlo en un ciclo.

En la primera parte sería muy fácil, solo haces esto:

name_subtypes[attr_subtype]

En la segunda parte tendrías que hacer esto:

for i = 0, table.getn(name_subtypes) do
	if attr_subtype == name_subtypes[i] then
    	return attr_subtype
    end
end

La razón es porque, si le tienes un id a cada objeto de tu tabla, solo accedes a él normalmente con tabla[id]. En cambio si no tienes un id para tus objetos en la tabla, tienes que recorrerla y comparar hasta encontrar la coincidencia.

 

Dejo aquí el croquis de donde vamos:

item_editor_camilo_lib.getAvailableAttr = function()
	local id_types_base = item_editor_camilo_lib.getApplyTypeBase()
	local r, item_attr_table = mysql_direct_query(string.format("SELECT apply as name_apply, prob, lv1, lv2, lv3, lv4, lv5 FROM player.item_attr WHERE %s > 0;", item_editor_camilo_lib.getNameSubtype(item.type, item.sub_type)))
  --Vamos aquí
	local available_attr = {}
	for i = 1, r do
		local name_apply = item_attr_table[i].name_apply
		local id_apply = apply[name_apply]
		if not item_editor_camilo_lib.isAttrIncluded(id_apply, id_types_base) then
			table.insert(available_attr, {})
			local current_pos = table.getn(available_attr)
			available_attr[current_pos][0] = id_apply
			available_attr[current_pos][1] = name_apply
			available_attr[current_pos][2] = apply_human[id_apply][2]
			available_attr[current_pos][3] = {}
			for j = 1, 5 do
				available_attr[current_pos][3][j] = item_attr_table[i]["lv"..j]
			end
			available_attr[current_pos][4] = item_attr_table[i].prob
		end
	end
	return available_attr
end

Con la tabla de los tipos de bonus base y los tipos de bonus que pueden salir por cada equipo, sacaremos los tipos de bonus disponibles en nuestro ítem y los meteremos en una tabla.

Vamos de 1 hasta r (recuerden que es la cantidad de registros que arroja la tabla de la query) e insertaremos en orden los bonus así:

Posición 0: el id, es decir, el número que aparece en constants.cpp

Posición 1: el nombre del apply, o sea, el nombre en mayúscula en inglés separado de guiones bajos que está en constants.cpp

Posición 2: el nombre que se mostrará a la persona en el juego, yo lo llamo humano, o sea, leíble para los humanos.

Posición 3: los valores lv1 hasta 5

Para el nombre es muy fácil:

local name_apply = item_attr_table[i].name_apply

Yo lo expliqué cuando había un solo registro, que era poniendo [1].

Para cuando son varios registros estamos usando este ciclo for con la variable de control i.

Para el id_apply: ahora sí iremos a la tabla apply del questlib.lua. Si la revisas, te fijas que en las [clave] = valor, en clave está el nombre del tipo de bonus que está en constants.cpp

Solo accediendo a la posición del bonus obtendremos el id, es muy sencillo.

Lo importante de este ciclo es filtrar esos tipos de bonus base, así que debemos identificarlos.

if not item_editor_camilo_lib.isAttrIncluded(id_apply, apply_type_base) then

Cada iteración del ciclo hará referencia a un bonus.

En cada bonus evalúa si el ítem ya lo tiene incluido o no en el item_proto.

Literalmente dice: si no está el atributo incluido, entonces

En los parámetros pongo el id_apply y id_types_base porque voy a tomar el bonus actual y lo voy a comparar con los bonus de esa tabla; si lo encuentra entonces retornará true. Si no, entonces hay que agregarlo a nuestra tabla de disponibles.

Veamos cómo se hace eso:

item_editor_camilo_lib.isAttrIncluded = function(id_apply, table_attr_base)
	for _, id in pairs(table_attr_base) do
		if tonumber(id) == tonumber(id_apply) then
			return true
		end
	end
	return false
end

Creamos la función isAttrIncluded.

Uso del pairs:

En los ciclos no siempre se recorre desde un valor hasta cierto otro valor contando de 1 en  1 o de 2 en 2.

A veces necesitamos recorrer cada elemento existente.

Digamos que tenemos una tabla así:

local tabla = {
	[1] = "uno",
	[2] = "dos",
	[3] = "tres",
	["uno"] = "UNO",
	[100] = "cien"
}

Si ponemos un for así:

for i = 1, table.getn(tabla) do --pues tiene 5 elementos
	chat(tabla[i])
end

La salida sería:

uno
dos
tres
--aquí daría error, no existe la posición 4
--aquí error, no existe la posición 5

Faltaría el UNO y el cien además.

Para mostrar solo los números haríamos esto:

for k, v in ipairs(tabla) do
	--key: value
	--clave: valor
 	chat(k..": "..v)
end

La salida sería:

1: uno
2: dos
3: tres
100: cien

Así es como funciona el ipairs.

Ahora falta otro, el pairs.

for k, v in pairs(tabla) do
	--key: value
	--clave: valor
  	chat(k..": "..v)
end

La salida sería:

1: uno
2: dos
3: tres
uno: UNO
100: cien

Ven la diferencia? realmente no sé si el orden sea ese pero lo importante es que pairs muestra tanto posiciones con texto como números, y el ipairs muestra solo posiciones de números.

Yo puse pairs en la quest pero da igual porque no hay posiciones en texto.

Cuando pongo if alguna_variable then, estoy diciendo que si la variable es nula o tiene algo que no sea false, entrará al if.

La única razón para que no entre es que el valor de la variable sea false.

if not item_editor_camilo_lib.isAttrIncluded(id_apply, id_types_base) then

Si el actual bonus del ciclo no se encontró en la tabla de bonus base del objeto, entonces retona false y entrará al if.

La estructura de mi tabla de disponibles será así:

[1] = { --ese 1 es la posición que se agrega automáticamente por hacer el table.insert.
	[0] = id_apply, --ej: el 1
  	[1] = name_apply, --el nombre del POINT, el que está en constants.cpp, ej: "MAX_HP"
  	[2] = nombre_humano, --quiero decir, el bonus que lo pueda leer un humano y lo entienda, ej: "Máx. HP"
  	[3] = {
		[1] = value_bonus1, --estos son los levels de player.item_attr
		[2] = value_bonus2,
		[3] = value_bonus3,
		[4] = value_bonus4,
		[5] = value_bonus5
	}
}

Lo hago poniendo el index en orden porque será útil más adelante.

Se preguntarán que por qué no pongo simplemente:

available_attr[i][0] = id_apply,

por ejemplo. Pues intentaré explicarlo como pueda.

Como hay un if que me restringe algunos bonus, si yo pongo un if, mi tabla ya no estará en orden.

--vamos con la i = 1
--este bonus no lo tiene, ok, agregado
[1] = {--[[todo lo del bonus aquí--]]}, 
-- i = 2
--este bonus no lo tiene, ok, agregado
[2] = {--[[todo lo del bonus aquí--]]}, 
-- i = 3
--este bonus sí está en la tabla, no entra al if, no se agrega

-- i = 4
--este bonus no lo tiene, ok, agregado
[4] = {--[[todo lo del bonus aquí--]]},

--El orden quedó 1, 2, 4... y no nos servirá para nuestro ciclo de más adelante

En vez de la i puse que se insertara la nueva tabla en la última posición y fue sencillo:

table.insert(available_attr, {})
local current_pos = table.getn(available_attr)
available_attr[current_pos][0] = id_apply

Obtengo el tamaño de mi tabla, y obviamente ese tamaño será la última posición -1.

Ahora la parte del apply_human. ¿Qué significa eso?

available_attr[current_pos][2] = apply_human[id_apply][2]

Es una tabla global (creada en el questlib.lua) hecha por mí y adjunta abajo al final, en donde defino nuevamente los bonus del apply pero en una estructura diferente y con los nombres que aparecen en el locale o muy parecidos. Es decir, en vez de que salga "MAX_HP", que salga "Máx. HP". Otro ejemplo, en vez de que salga "CRITICAL_PCT", salga "Probabilidad de golpes críticos +x%". El % también lo incluyo. Estos textos no aparecen en los bonus del ítem obviamente, recuerden que esto es quest, solo aparecerá en la pantalla negra en los botones.

Se puede crear una tabla dentro de otra. En cada bonus creé esa estructura que puse arriba. También hice una nueva para los levels, ya que son 5, y son en orden, la mejor manera era crear una tabla.

Ya tenemos los bonus disponibles. Ahora sí volvamos a addAttr

item_editor_camilo_lib.addAttr = function(bonus_count)

	available_attr = item_editor_camilo_lib.getAvailableAttr()
	item_editor_camilo_lib.makeSelectUI() --vamos a crear ahora el select
end

He creado la variable available_attr como global ya que la usaré en otra función sin tener que enviarla por parámetro.

Crearemos el select_table. La diferencia entre un select y un select_table es que en el select envías los textos separados por coma, mientras que en el select_table envías una tabla.

Las tablas son dinámicas.

--Si quisieras poner los números del 1 al 5 en texto en un select, lo harías así:
local sel = select("Uno", "Dos", "Tres", "Cuatro", "Cinco")
--Podría cambiar los números sin problema al ponerlo en variables
local num1, num2, num3, num4, num5 = "uno", "dos", "tres", "cuatro", "cinco"
local sel = select(uno, dos, tres, cuatro, cinco)
--Pero si quisiera variar la cantidad de textos que hay en el select, ahí ya no se puede. Si solo necesito 3 textos, qué hago con los otros dos que me sobran?
--A ese problema nos enfrentamos, pues cada equipamento tiene distintas cantidades de tipos de bonus que pueden salir. Unos tienen 14, otros 15, otros 16, etc.
--La solución es crear una tabla y usar select_table.
local tabla = {"uno", "dos", "tres", "cuatro", "cinco"}
local sel = select_table(tabla)
--Ahora quiero dejar solo tres números.
local tabla = {"uno", "dos", "tres"} --ya sea que lo haya cambiado con un insert o asignaciones, lo que sea.
local sel = select_table(tabla)

Ahora sí vamos con el makeSelectUI

item_editor_camilo_lib.makeSelectUI = function()
	type_select_ui, value_select_ui = {}, {}
	for i = 1, table.getn(available_attr) do
		type_select_ui[i] = available_attr[i][2]
		for j = 1, table.getn(available_attr[i][3]) do
			table.insert(value_select_ui, {})
			value_select_ui[i][j] = available_attr[i][3][j]
		end
		table.insert(value_select_ui[i], "Volver")
	end
	table.insert(type_select_ui, "Cerrar")
end

Creamos dos tablas, una para los select_table de los nombres en humano de los bonus y otra para los valores en el select_table.

Aquí no hay mucha ciencia, solo tomamos el available_attr que es global, y agarramos la pos 2 que es el nombre humano y la pos 3 que son los valores de ese bonus. Hay que darle un valor añadido y es, una vez llenado los valores de la página del bonus, agregarle el botón de "Volver". También una vez agregados todos los bonus, agregar el botón de "Cerrar".

 

Vamos a la última parte:

item_editor_camilo_lib.addAttr = function(bonus_count)

	available_attr = item_editor_camilo_lib.getAvailableAttr()
	item_editor_camilo_lib.makeSelectUI()
 	--Vamos de aquí para abajo. La función anterior no retorna porque las tablas las dejé globales.
	local type_saved, value_saved = {}, {}
	
	local type_support = {}
	for i = 1, table.getn(available_attr) do
		type_support[i] = available_attr[i][0]
	end
	table.insert(type_support, "")
	
	for i = 6-bonus_count, 5 do
		say_title(string.format("Elige el %s° bonus", i))
		local sel_type = select_table(type_select_ui)
		if sel_type != table.getn(type_select_ui) then
			say_title("Elige el valor")
			local sel_value = select_table(value_select_ui[sel_type])
			if sel_value != 6 then
				table.insert(type_saved, type_support[sel_type])
				table.insert(value_saved, value_select_ui[sel_type][sel_value])
				table.remove(type_select_ui, sel_type)
				table.remove(value_select_ui, sel_type)
				table.remove(type_support, sel_type)
			else
				i = i - 1
			end
		else
			return
		end
	end
	
	local count = 1
	for i = math.abs(bonus_count-5), 4 do
		item.set_value(i, type_saved[count], value_saved[count])
		count = count + 1
	end
	syschat("Hecho!")
end

Esas tablas type_saved y value_saved son los bonus y los valores del bonus que selecciona la persona. Saved significa guardados o salvados.

local type_support = {}
for i = 1, table.getn(available_attr) do
	type_support[i] = available_attr[i][0]
end
table.insert(type_support, "")

Creé una tabla de soporte simplemente por cuestiones de conversión. Necesitaba obtener el id del bonus pero el type_select me da el nombre humano, y el select_table me da la pos de ese nombre humano pero no el id del bonus. Así que hice una tabla del mismo tamaño del select porque también necesitaba eliminarla para cuando la persona seleccionara un bonus. Me explico, cada vez que la persona elije un bonus hay que quitarlo para que no aparezca en las demás páginas porque no se debe repetir. Es por eso que más abajo aparece table.remove.
 

La tabla de soporte tiene esta estructura:

type_support = {
	[1] = 1, --id del bonus
	[2] = 2,
	---...
	--no siempre será en orden, ya que hay bonus que se saltan
}

Puse:

table.insert(type_saved, type_support[sel_type])

y no:

table.insert(type_saved, available_attr[sel_type][0])

porque available_attr como es global, no puedo eliminarle bonus porque puede que interfiera con otro personaje. Así que creo la de soporte para que se le pueda eliminar los bonus.

 

En verdad puse variables globales en varias partes cuando no debería porque era para explicar lo de global y local.
Lo explicaré mejor. Si es una variable que cambiará por cada personaje, entonces ponla local.
Si la variable se usará por otros personajes , por ejemplo en un evento, entonces ponla global.

Explico de nuevo. En este caso el available_attr que lo había puesto global, imagina que dos personas estén usando al tiempo el editor. Puede llegar un momento en que al crear la tabla support de alguno de los dos, tome el available_attr de la otra persona y sean diferentes.
 

En el archivo adjunto no habrá variables globales a excepción de switch_max_amount ya que ese valor no cambia. La llamaría una constante.
 

En esta parte:

for i = 6-bonus_count, 5 do

es muy sencillo, tienes que tener en cuenta que como esto es la interfaz, esto será equivalente a las páginas de bonus que le mostrarás al personaje.


Si hubieras puesto:


bonus_count = 5 -> 6-5 = 1 hasta 5. Página 1 hasta la 5
bonus_count = 4 -> 6-4 = 2 hasta 5. Página 2 hasta la 5. Esto en caso de que hubiera salido algún valor 0 de media o habilidad.
bonus_count = 3 -> 6-3 = 3 hasta 5. Página 3 hasta la 5. Esto en caso de que salga valores de media y habilidad diferentes de 0.

 

Para cuando le dan al último botón siempre uso mi viejo truco de poner:

if sel != table.getn(tabla) then
    return
end

En este caso en el sel_type pongo que si le da a "Cerrar", cancele toda la operación.
En el caso del sel_value pongo diferente de 6 porque siempre los levels van a ser 5, y el botón 6 es "Volver", así que le resto 1 a mi variable de control para que me devuelva a la página anterior.

Ya en la última parte es necesario hacer un truco para sincronizar la cantidad de bonus con las posiciones donde se van a poner.

for i = math.abs(bonus_count-5), 4 do

Si bonus_count = 5 -> de 0 hasta el 4. Ahí están los 5 bonus.
Si bonus_count = 4 -> de 1 hasta el 4. Como ya tiene un valor de media o habilidad diferente de 0, se agregarán los bonus del segundo al quinto.
Si bonus_count = 3 -> de 2 hasta el 4. La media y habilidad es diferente de 0, ya tiene dos bonus, se agregan los otros 3.

Ese math.abs es una función que ya viene con lua y en todos los lenguajes de programación. En matemáticas el absoluto es volver positivo todo valor.
Si no hubiera puesto el absoluto, miren lo que hubiera ocurrido:

Si bonus_count = 5 -> de 0 hasta el 4. Aquí está bien porque da 0
Si bonus_count = 4 -> de -1 hasta el 4. Del -1 al 4 hay 6, serían 6 bonus.
Si bonus_count = 3 -> de -2 hasta el 4. Del -2 al 4 hay 7, serían 7 bonus.

En esta línea:

item.set_value(i, type_saved[count], value_saved[count])

Podemos ver que la i debe empezar en 0, y las tablas type_saved y value_saved en 1. Ya saben por qué.
Con ese local count = 1 lo hago.
Y lo aumento en 1 en cada iteración y ya está.

La estructura de type_saved es:

type_saved = {1, 18, 17, 16, 22}

La estructura de value_saved es:

value_saved = {2000, 10, 10, 10, 20}

Donde el 1 corresponde con el 2000, el 18 con el 10 y así.

Al final pongo "Hecho!" para comprobar que se terminó el editor.
 

 

Los resultados son los mismos que en el post original del editor.

 

(dejo todo en el adjunto)

 

Muchos éxitos para todos!

 

Post original de la quest: 

 

 

Debes iniciar sesión para ver el contenido del enlace en esta publicación.

Share this post


Link to post
Share on other sites

Esta interesante el post. Aprender Lua es la parte "básica" para crear misiones, dungeons y tal en un server de metin2 y no es tan complicado, con práctica (y muchos errores después xD) vas mejorando para hacer una misión, evento o dungeon básico y tal

Share this post


Link to post
Share on other sites
hace 59 minutos, VIKSANT dijo:

Hola man, hay alguna manera de poder ponerme en contacto contigo? la verdad que quiero hablar contigo un tema muy muy concreto y breve.

Lo agradecería de corazon. Muchas gracias,

Sí, claro, con gusto

Mi Discord es Camilo#0869, siempre estoy activo

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

Sign in to follow this  

  • Recently Browsing   0 members

    No registered users viewing this page.

×
×
  • Create New...