Saltar al contenido

Más en DLAN: Nuestras Traducciones | Otras Traducciones | Mods y modding | Revisiones y Guías | Videojuegos | Arte | Literatura | Rol y Rol por foro e IRC | Mapa de la web
Foto

Tutorial de scripting NWN


  • Por favor, ingresa para responder
11 respuestas al tema

#1 Setaka

Setaka

    Ancillae

  • Miembro
  • PipPipPip
  • 362 posts

Posteado 03 June 2015 - 11:50 PM

¡Empezamos! Cada post debería ser un tema. En este primer post pondré links para que se haga más fácil el navegar por ellos.
 
Mantened el tema limpio, para comentar cosas del mismo
, aquí

Se recomienda armarse con una taza de café antes de leer... u otras substancias estimulantes   :histeria:  

0. Antes de empezar...
1. El editor de scripts
2. Iniciar los scripts. Eventos
3. Funciones(y parámetros)
4. Operaciones
5. Haciendo un script

6. Condicionales (if, else, else if, switch)

7. Bucles (while, for, do)

8. Variables locales

9. Funciones propias

10. Conversaciones


Editado por Setaka, 09 August 2015 - 11:27 PM.

Tutorial NWN Scripting: Click aquí


#2 Setaka

Setaka

    Ancillae

  • Miembro
  • PipPipPip
  • 362 posts

Posteado 03 June 2015 - 11:55 PM

0. ANTES DE EMPEZAR...

 

 

star3.png Esta es la biblia del NWN y la herramienta más útil del scripter: NWN Lexicon. Explica todo lo que se puede hacer con el editor de scripts, por lo que si no sabéis utilizar una función solo tenéis que buscarla aquí y aparte de explicaros cómo funciona suele haber un ejemplo. En el menú de la izquierda, Lyceum, incluso hay más tutoriales de scripting básico.

star3.png El scripting no es más que aprender a dar órdenes a un robot en un lenguaje que entienda. El robot es muy listo y hará todo lo que le digas, pero tiene poca imaginación... así que no hará algo que no le hayas dicho. Por ejemplo, si quieres que se ejecute una acción debe saber a quien se aplica... ¡No va a adivinarlo!

star3.png El tutorial os puede ayudar, pero solo aprenderéis abriendo scripts simples ya hechos, leyéndolos y modificándolos; hasta que un día os atreveréis a hacer uno simple desde 0. Es solo un juego de lógica y practicar.

star3.png Es normal que haya errores al compilar mientras se trabaja. Al principio lo mejor es ir compilando (guardar) a casi cada línea que se añada y ver si da algún error. Si da error, saber leer la información que os da al respecto.

star3.png Es buena idea crear un módulo solo para scripts, agiliza su carga y se puede testear en cuestión de segundos. No hace falta ni que haya haks, salvo queráis scriptear algo que requiera su uso.

star3.png Si el script compila bien pero en el juego no funciona como debería, lo mejor es añadir mensajes de testeo para averiguar donde falla.

star3.png Es importante saber optimizar evitando código redundante, cuanto menos código haya y cuantas menos veces se ejecute mejor. Facilita la carga del mod y se utilizan menos recursos una vez subido.

star3.png Muchos no somos scripters y aprendimos solos, así que los scripters profesionales que no se echen las manos en la cabeza :P


Tutorial NWN Scripting: Click aquí


#3 Setaka

Setaka

    Ancillae

  • Miembro
  • PipPipPip
  • 362 posts

Posteado 04 June 2015 - 12:05 AM

1. EL EDITOR DE SCRIPTS
 
 
¡Toca conocer la mesa de trabajo!

Para abrir el editor de scripts: Clic secundario en Guiones->Nuevo; abrir un script existente; desde la pestaña de cualquier evento; o CTRL+ALT+S.

Los iconos de arriba no tienen ninguna complicación, es como un editor de texto cualquiera. De estos os explico solo algunos detalles que se pueden pasar por alto y son muy útiles.

  • En Abrir, saldrán todos los scripts personalizados del módulo. Esto es solo los que hemos creado nosotros. Si marcamos que muestre todos los recursos, también saldrán los scripts del juego, por ejemplo los conjuros, algunas dotes especiales, scripts del sistema por defecto, entre muchos otros (la mayoría paja...). Abajo, en "Nombre del recurso", puedes escribir para buscar teniendo en cuenta el nombre de cada script. Prueba a buscar el conjuro de "Bola de fuego".
  • Otra herramienta especialmente útil es la de "Buscar en archivos", marcando la opción de "Buscar en todos los archivos del módulo", para buscar texto en todos los scripts del módulo. Aquí a diferencia del anterior tiene en cuenta el contenido de cada script, no solo el nombre... Pero solo busca entre los scripts propios, no tiene en cuenta los del juego (mejor... ¡Ush ush! ¡Fuera fuera!).
  • Y una opción más, es buscar el conjuro o dote en la nwnwikia, al final de la página siempre nos detalla el script (si es de código abierto)

Vemos qué más tenemos en el editor de scripts...

En el centro tenemos la pantalla para escribir el texto. Ahora mismo solo pone void main(), esta es la función nula. Entre los { } añadiremos nuestro código. Es importante ver el uso de los {}. Solo cuenta el código que haya dentro. Y siempre, siempre que se use un {, debe cerrarse más tarde con un }. También con los ( y ).

¿Cómo añadimos nuestro código? Lo vemos.

Click en Funciones. Salen todas las funciones básicas del juego. Si se clica una vez en cualquiera de ellas, abajo se puede leer la información de la misma. Si se clica dos veces, se copia y pega en el editor de texto, ahí donde habíamos escrito por última vez. Más adelante veremos qué significa toda la información que nos dan de una función, si se entiende perfectamente esto lo siguiente está chupado. Y no es algo difícil. Clicad algunas al azar. Por ahora solo tenéis que quedaros en que las funciones donde pone "void" delante son acciones, y las que pone otra cosa son funciones que obtienen información necesaria para realizar dichas acciones. Por lo tanto, sí, el void main() es la función principal donde le decimos todas las órdenes.

Arriba tenemos lo de Filtrar. Otro buscador, superútil. Escribid effect... salen todos los efectos. Escribimos Get... Salen funciones para obtener cosas. Ahorra mucho tiempo. Además aquí podemos escribir acentos y en el editor de scripts no, por lo que si necesitamos acentos cuando trabajamos con texto, podemos escribirlos aquí y pegar el texto en el código.

Ahora click en Variables... Mhhh... ¿Nada? icon_razz.gif Aquí se muestra solo el nombre de las funciones que hemos definido con un nombre. Imaginaos que queremos aplicar muchas acciones en un PJ o obtener mucha información de él. Para decirle a nuestro robotito obediente que nos busque el PJ, usaremos ahora la función GetFirstPC(), que lo que hace es detectar el PJ (cuidado, esta solo en módulos singleplayer o multijugador solo en el caso de que solo haya un jugador). Leemos lo que pone la información de esta función:

// Get the first PC in the player list.
// This resets the position in the player list for GetNextPC().
object GetFirstPC()
 

Nos dice que esta función nos devuelve un objeto (en este caso, el "objeto" PJ). En vez de estar utilizando la función una y otra vez (en vez de decirle al robotito "Búscame al PJ para que diga tal cosa", "Búscame al PJ para mirar si está armado", "Búscame al PJ para ver si es humano...") lo que hacemos es ponerle un nombre propio, para que con buscarlo una vez sea suficiente y nos sea más cómodo. Aquí en el ejemplo, lo he llamado oPC, como podría haberlo llamado paquito. Una vez grabamos el script, sale el oPC en la pestaña variables.
 

void main()
{
object oPC = GetFirstPC();
}


Ahora si tengo que utilizar muchas funciones sobre este PJ, ya solo tengo que decirle a nuestro querido robot: "Haz que oPC diga tal cosa", "Mira si oPC está armado", "Comprueba si oPC es humano...". Fijaos como lo he hecho para definirlo. Un = para decirle que me considere igual lo que hay a ambos lados del símbolo (¡Igual que en mates!), el ( se ha cerrado con el ), y ; para decirle que esta línea se ha terminado. Y le hemos dicho que oPC es object, porque en la información de la función dice que esta obtiene un object.


En la pestaña Constantes salen pues eso... Constantes. Básicamente es información numérica a la que han puesto un nombre para que nos sea más fácil. Buscad la información de la función GetAbilityScore e intuiréis como usarlo. ¡El buscador también sirve para las constantes! Por ejemplo si buscáis "VFX" encontrareis todos los efectos visuales que pueden usarse en la función EffectVisualEffect.


Y la pestaña de Plantillas... Totalmente innecesaria icon_rolleyes.gif ...


Tutorial NWN Scripting: Click aquí


#4 Setaka

Setaka

    Ancillae

  • Miembro
  • PipPipPip
  • 362 posts

Posteado 04 June 2015 - 12:12 AM

2. INICIAR LOS SCRIPTS. EVENTOS
 
Lección muy fácil si ya habéis trasteado un poco.

En las propiedades del módulo o de cualquier área, ubicado, criatura, puerta, desencadenante, transición o trampa (no sé si me dejo alguno...) hay una pestaña llamada "Sucesos" o "Guiones". En ella hay una lista de los eventos que puede desencadenar el objeto. También hay eventos en la pestaña "Trampa", en el caso de trampas y ubicados.

Por ejemplo, si entramos como PJ en el módulo, una área, desencadenante, trampa... Se activa el evento OnEnter, y ejecutará el script que tenga asignado en este evento.

Así pues, si quisiéramos...
... dar XP por quitar una trampa, usaríamos el evento OnDisarm de una trampa.
... que un cofre explotara al abrirlo, evento OnOpen del ubicado.
... que saliera un mensaje al usar un ubicado, evento OnUsed. O OnClick si el PJ no tuviera que tocarlo.
... que una criatura gritara algo al morir, evento OnDeath.
... una criatura con un efecto visual o conjuro activado nada más salir, evento OnSpawn.
... que al entrar en una área nos saliera un mensaje, OnEnter del área.
... que al pasar por cierta zona de esta área nos saliera otro mensaje, OnEnter de un desencadenante.

Con leer los eventos ya se sabe cuando son llamados, id mirando los distintos eventos que hay. Hay algunos que no son tan claros y que dejo aquí escritos:

OnHeartBeat: "Al latir el corazón". Esto es un evento que se llama cada 6 segundos, por lo que el script que se ponga en este evento estará siempre funcionando. Esto depende de como sea el script produce mucho lag y siempre es mejor evitarlo. Se ha criminalizado bastante su uso pero tampoco es tan malo como dicen, pues bien que toda criatura en el módulo usa un script en este evento y no ligero precisamente. Poner que toda farola en el módulo se apague de día y se encienda de noche poniendo un script en el evento de cada una de ellas para que vaya comprobando si es de día o noche y encienda/apague, es un script sencillo pero... pongamos que hay 100 farolas en el módulo... ¡ERROOOOOOR! Poner un script en una criatura para que compruebe cada 6 segundos si cerca de ella hay un ubicado "x" del que extrae poder para curarse... ¡¡ACIERTOOOO!!. Todo está en saber usarlo bien. Las farolas siempre están ahí chupando recursos (lo mismo en los objetos Modulo, Área, Desencadenante...), pero la criatura bien que podemos seleccionar cuando spawnearla, y puede morir, anulando el script; además que podría ser única en todo el módulo (boss), por lo que solo sería un script ejecutándose.

OnUserDefined: Tiene a ver con crearte un evento personalizado, pones aquí tu script, y lo llamas desde otro script con la función EventUserDefined.

Avanzado: Cómo usar este evento para IA de criaturas, by lavafuego

Spoiler


OnDisturbed: Cuando trasteamos el inventario de un ubicado o criatura, ya sea quitando o dando un objeto.

OnPlayerDying, OnPlayerDeath, OnPlayerRespawn: El primero es cuando el personaje está muriendo (los PG negativos, el segundo cuando la ha palmado y le debería salir la pantalla de resurrección, el tercero cuando pulsa "Regenerar" en dicha pantalla.

OnCutsceneAbort: Cuando el jugador anula una secuencia cinematográfica.

OnBlock: Cuando las criaturas quedan atrapadas por una puerta (la IA hace que la abran).


star3.png Otra forma de iniciar un script es llamándolo desde otro script con la función ExecuteScript.
 

// Make oTarget run sScript and then return execution to the calling script.
// If sScript does not specify a compiled script, nothing happens.
void ExecuteScript(string sScript, object oTarget)

 
Por ejemplo, al activar el poder de un objeto se activa el evento OnActivateItem del módulo, y en este script en teoría tendría que ir todo el código de todos los objectos activables. En cambio, se usa la función ExecuteScript para que ejecute el script con el mismo nombre que la etiqueta del objeto activado, de forma que tenemos un script aparte para cada objeto, lo que lo hace bastante más sencillo y menos pesado de leer. Algo como:
 

void main()
{
//este script seria llamado en el evento OnActivateItem del modulo
object oItem = GetItemActivated();//objeto activado
object oPC = GetItemActivator();//quien lo ha activado
string sItem = GetTag(oItem);//etiqueta del oItem
ExecuteScript(sItem, oPC);//ejecuta script sItem (resultado del GetTag(oItem)) al oPC
}

 
Si el objeto activado tiene de etiqueta "chocolatina" pues ejecutará el script "chocolatina".

Importante si queremos añadir algo a un evento sin que se pierda el script que ya hay por defecto, algo común en criaturas, porque no queremos modificar su IA. Si queremos añadir por ejemplo algo en su OnHeartBeat, en vez de crear un nuevo script copiando y pegando el código del script que ya tiene puesto por defecto y añadir nuestro código, sería mucho mejor crear nuestro script, y después usar el ExecuteScript para llamar al script que tenía antes. Si ya tenemos el código escrito no tiene sentido volverlo a escribir.

Siguiente lección... cómo usar las funciones, y qué diantres es esto de object, string, int...


Editado por Setaka, 28 July 2015 - 01:24 PM.

Tutorial NWN Scripting: Click aquí


#5 Setaka

Setaka

    Ancillae

  • Miembro
  • PipPipPip
  • 362 posts

Posteado 04 June 2015 - 12:27 AM

3. FUNCIONES (y parámetros)
 
Qué es una función? Pues lo que sale en la pestaña de "Funciones" (¡toma ya!) que ya hemos visto antes. Simplemente ejecutan acciones o recogen información. Si clicais en algunas de ellas, en la pestaña información veréis qué hace cada una de ellas. Las que realizan acciones son las void, que realizan acciones y no devuelven ningún valor, y las otras (object, int, string... ahora lo veremos) recogen información y devuelven el valor. Así pues, los scripts que empiezan por void main(), lo que realmente estamos haciendo es configurar una gran función para que realicen una acción que definamos; es la función principal.
 
Un ejemplo de función void:
 

// Apply eEffect to oTarget.
void ApplyEffectToObject(int nDurationType, effect eEffect, object oTarget, float fDuration=0.0f)

Esta dice que es una función void. Nos sirve para aplicar un efecto a quien definamos (una acción). Y lo que hay entre paréntesis, es para configurar esta función: Lo que llamamos parámetros. Por esto el título de este tuto es Funciones(y parámetros). Fácil de recordar, ¿no?  :thumb:  En esta función podemos definir los parámetros de la duración del efecto, qué efecto queremos (visual, aumenta CA, baja habilidad, daño...), a quién se lo aplicamos, y cuanto dura si hemos elegido que el efecto sea solo temporal (ej: el efecto de un conjuro).

 

Una de las cosas más importantes en el scripting es saber interpretar los parámetros de una función, ya que es cuando realmente se pasa de copiar trozos de scripts de otros a poder hacer los tuyos propios. Y cómo veis, no tiene demasiado secreto, ya que aún teniendo dudas de qué significa cada parámetro, podemos visitar la página de la función en el NWNLexicon para que nos destripe qué significa cada uno.


Ahora ejemplo de una función que devuelve un valor:

// Create a Damage effect
// - nDamageAmount: amount of damage to be dealt. This should be applied as an
//   instantaneous effect.
// - nDamageType: DAMAGE_TYPE_*
// - nDamagePower: DAMAGE_POWER_*
effect EffectDamage(int nDamageAmount, int nDamageType=DAMAGE_TYPE_MAGICAL, int nDamagePower=DAMAGE_POWER_NORMAL)

 
Esta función dice que es effect, así que devuelve un efecto. ¡No sirve para aplicar el daño, ya que sino sería void! Sirve para definir un efecto dañino, pudiendo configurar en sus parámetros la cantidad de daño, el tipo, y su potencia. ¿Y cómo aplicamos el daño? Pues con la función anterior, que uno de sus parámetros era definir el efecto a aplicar, pues con esta función lo definimos. Es decir, que las funciones también pueden ser parámetros de otras funciones (y estas a la vez parámetros de otra, que pueden ser a la vez parámetros de otras, que pueden ser parámetros de otras que pueden ser......  :fuego: ).



star3.png Vemos ahora los distintos tipos de funciones o variables:

void: Acción. La única que no devuelve un valor.
Ej: ActionExamine(object oExamine); DestroyObject(object oDestroy, float fDelay=0.0f)

 
int: De integrer. Devuelve un valor entero.
Ej: 3, 8, 9890.
Ej función: GetAge(object oCreature); d100(int nNumDice=1)
No todo son siempre números, si vais a la pestaña de "Constantes" veréis que lo que hay en realidad también son números (que corresponden a filas de 2das), simplemente que le han definido un nombre para que sea más fácil trabajar con ellos.


float: Devuelve un valor decimal.
Ej: 3.9, 4.0001, 5683.0
Ej función: GetFacing(object oTarget) sirve para obtener la orientación de un objeto, de 0.0 a 360.0


string: Significa algo así como "cadena". Devuelve texto. ¿Y porqué he puesto "cadena"? Porque el texto que devuelve siempre va entre "".
Ej: "cadena", "I <3 Seta", "KmO mOlaH esoH dL tExTo pRimoH pUedoH pOnEh lO k KieRa"
Ej función: GetName(object oObject, int bOriginalName=FALSE); GetTag(object oObject)


object: ¿Hace falta que diga lo que es? Pues sí, un objeto puede ser desde todo lo físico del módulo, como PJs, DMs, PNJs, ubicados, puertas, objetos de inventario (items)... a puntos de ruta, desencadenantes... e incluso el objeto área o el objeto módulo.
Ej valor que devuelve: Un PJ, el área donde está dicho PJ, un ubicado, un item... o OBJECT_INVALID si no encuentra ninguno.
Ej función: GetArea(object oTarget), GetFirstObjectInArea(object oArea=OBJECT_INVALID)


effect: También uno fácil de intuir y que encima ya hemos visto.
Ej valor: Un efecto no se puede expresar en letras ni números... creo icon_lol.gif Simplemente devuelve el efecto que definas, por ejemplo antes la función de daño devuelve un efecto de daño. (en realidad sí podemos hacerle devolver un valor, con la función "int GetEffectType(effect eEffect)", nos devuelve qué efecto es (devuelve los valores que empiezan con EFFECT_TYPE en la pestaña de "Constantes"))
Ej función: EffectPolymorph(int nPolymorphSelection, int nLocked=FALSE), EffectSleep()


vector: Devuelve unas coordenadas (x, y, z). Y tanto la x como la y como la z están expresadas como floats.
Ej: (2.49, 4748.0, 0.0001)
Ej función: Vector(float x=0.0f, float y=0.0f, float z=0.0f) para definir uno manualmente; GetPosition(object oTarget) para que nos devuelva las coordenadas de un objeto.


location: Nos devuelve una localización. Es como un vector pero contiene más información, ya que tiene también la información del área donde se encuentra, y podemos definir la orientación. Pero si estamos trabajando con posiciones, es mejor usar vectores porque permiten que los modifiquemos más fácilmente (ej: obtener la posición justo al lado derecho de un PJ).
Ej: La localización de un punto de ruta, que precisamente se orienta en el aurora para que si teletransportamos el PJ a este punto, se oriente igual que él. Si lo teletransportaramos a un vector, el PJ se orientaría hacia el 0.0 (norte).
Ej función: Location(object oArea, vector vPosition, float fOrientation) para transformar un vector a un location. GetLocation(object oObject) para obtener la localización de un objeto.


itemproperty: Propiedades de objetos. Funcionan igual que los efectos, pero aplicados a items.
Ej: Vengadora sagrada, Afilada...
Ej función: ItemPropertyACBonus(int nBonus), ItemPropertyHolyAvenger()
En la pestaña "Constantes", Buscad IP y bajad... Ahí están definidas como números por si queremos saber qué propiedades tiene por ejemplo una armadura.


Hay más... ¡muchas más!: Aunque por suerte no demasiado utilizadas. http://www.nwnlexico...gory:Data_Types

 

 

Extra by lavafuego: Funciones que nos pasan de un tipo de variable a otro tipo de variable

Spoiler


Editado por Setaka, 19 July 2015 - 10:22 PM.

Tutorial NWN Scripting: Click aquí


#6 Setaka

Setaka

    Ancillae

  • Miembro
  • PipPipPip
  • 362 posts

Posteado 04 June 2015 - 12:44 AM

4. OPERACIONES (by Lavafuego & Setaka)

Antes de meternos de lleno en el script hay que aprender ciertas bases como las operaciones aritméticas. Ya sé que suena a chorrada, pero creedme que no lo es.
Tenemos ciertos símbolos para realizar operaciones:
 
+ añade algo

int iUno=1;
int iDos=2;
int iSuma= iUno+iDos; 

Luego el valor de iSuma es 3.
 
 
- resta algo

int iUno=1;
int iDos=2;
int iResta= iUno-iDos; 

Luego el valor de iResta es -1.
 
 
++ incrementa el valor de una variable int en uno

int iUno=1;
iUno++; 

Ahora iUno es 1+1=2
 
 
-- Disminuye el valor de una variable int en uno

int iTres=3;
iTres--; 

ahora iTres vale 3-1=2
 
 
* multiplica

int iTres=3;
int iDos=2;
int iResultado= iTres* iDos; 

luego iResultado es 3*2=6
 
 
/ divide

Int iSeis=6;
Int iDos=2;
Int iDivision= iSeis/ iDos; 

El valor de iDivision es 6/2=3
 
 
Y el = que lo usamos para asignar un valor que está a la derecha del símbolo a la expresión de su izquierda ejemplo:

int iEdad= 35;
int iEdadSecreta= iEdad; 

La int iEdad tiene un valor de 35 y la int iEdadSecreta tiene el valor de iEdad o sea 35.
 
 
Las operaciones pueden dar como resultados números enteros (int) o números decimales (float). Ejemplo:
3+2=5      número entero
3/2=1.5    numero float
Hay que tener en cuenta el resultado que va a dar, no resulta conveniente declarar un valor int a un resultado que va a ser un float . Ejemplo:

int itotal=3.5+2; //MAL
float fTotal=3.5+2; //BIEN
int iTotal=3+2; //BIEN
float fTotal=3+2; //MAL 

 
También podemos usar el símbolo + cuando trabajamos con texto (string). Es importante ver que cuando trabajamos con texto, este va dentro de comillas.
Por ejemplo, si queremos saludar al jugador cuando entre en el módulo, evento OnClientEnter:

void main()
{
object oPC = GetEnteringObject(); //quien entra
string sName = GetName(oPC); //nombre del oPC, ej "Fulano"
string sFrase="Hola "+sName; //resultado: "Hola Fulano"
SendMessageToPC(oJugador, sFrase); //una vez creada la cadena completa, enviamos
}

Nótese el espacio en blanco en el "Hola ", de no ponerlo el resultado de la suma sería "HolaFulano".
 
 

Now be ready... Que haremos un pequeño script con lo que hemos aprendido   :o 


Editado por Setaka, 27 June 2015 - 01:13 PM.

Tutorial NWN Scripting: Click aquí


#7 Setaka

Setaka

    Ancillae

  • Miembro
  • PipPipPip
  • 362 posts

Posteado 25 June 2015 - 04:06 PM

5. HACIENDO UN SCRIPT
 
 
Imaginemos que queremos crear una trampa mágica en una área. Que ni sea detectable. Y que obviamente dañe a quien la pise, por daño mágico.

Lo que haríamos sería crear un desencadenante, y cómo queremos que se aplique a quien la pise, el script iría en su evento OnEnter.

Queremos que aplique un efecto de daño, así que tenemos que utilizar la función que ya hemos visto:
 
// Apply eEffect to oTarget.
void ApplyEffectToObject(int nDurationType, effect eEffect, object oTarget, float fDuration=0.0f)
 
Empezamos a ver cómo configurarla... Normalmente explican qué es cada parámetro, pero esta se utiliza tanto que creo que pasaron de hacerlo xD Se tiene que saber sí o sí, y si no... Click aquí
 
Y obtenemos la información...
Parameters

nDurationType A DURATION_TYPE_* constant.
eEffect The effect to apply.
oTarget The target of the effect.
fDuration The duration of temporary effects. (Default: 0.0f)
 
nDuration: Vamos a la pestaña "Constantes". Buscamos DURATION_TYPE_ y como queremos un efecto de daño, pues INSTANT. El Instant es para efectos que con que se apliquen una vez ya es suficiente (efectos de usar y olvidar). El temporary si queremos mantener el efecto un tiempo (ej: el efecto de un conjuro). Y el permanent si no queremos que se quite salvo vía script cuando queramos nosotros.
 
eEffect: El efecto que queremos. Queremos un efecto de daño, así que tenemos que definirlo antes con una función que ya hemos visto (EffectDamage)
 
// Create a Damage effect
// - nDamageAmount: amount of damage to be dealt. This should be applied as an
//   instantaneous effect.
// - nDamageType: DAMAGE_TYPE_*
// - nDamagePower: DAMAGE_POWER_*
effect EffectDamage(int nDamageAmount, int nDamageType=DAMAGE_TYPE_MAGICAL, int nDamagePower=DAMAGE_POWER_NORMAL)
 
nDamageAmount: Cantidad de daño, y es un integrer. Le podríamos meter 10 y a por el siguiente parámetro. Pero queda muy soso... Qué tal si metemos aquí la función d10() (buscadla vosotros, ¡Vagos!) y que sea aleatorio? Pero esperad... que es muy aburrido que solo pueda dañar por 1 mísero punto. Metámosle un daño base de 5 y que se sume un d6 al daño (empezáis a sentir el poder que tenéis sobre los pobres jugadores verdad? Pronto empezareis a disfrutar el torturarles  :demonio: )

nDamageType: Queremos daño mágico.... Uh? Wait! Si en la función ya nos lo han definido ellos mismos! Así que podríamos pasar de este parámetro y del siguiente, que ya están ambos definidos como daño mágico y normal. Ahora bien, si quisiéramos definir iDamagePower nos veríamos obligados a definir también nDamageType, ya que tenemos que conservar el orden de los parámetros. Y evidentemente, si quisiéramos daño por fuego normal, tenemos que cambiar nDamageType, y no hace falta poner nada en el nDamagePower porque ya está definido cómo normal.


oTarget: A quien afecta. Queremos que afecte a quien entre en la trampa, que es quien entra en el desencadenante, así que lo definimos con la función GetEnteringObject(). Fijaos que no tiene ningún parámetro a definir, esto es porque no necesita. Si la usamos en el script del evento de un desencadenante, actúa sobre este. Si lo usamos en una área, sobre esta. Si no definimos ningún objeto, significa que se aplica en el objeto cuyo evento hemos llamado. Es decir, el propio objeto, lo que se llama como OBJECT_SELF.

No necesitamos nada más. Esto si lo ponemos del tirón quedaría así...
void main()
{
ApplyEffectToObject(DURATION_TYPE_INSTANT, EffectDamage(d6()+5), GetEnteringObject());//PJ, sufreeee!!!!
}
 
Pero... mejor ponerlo ordenadito:
 
void main()
{
object oPC = GetEnteringObject();//quien entra

int iDam = d6()+5; //puntos de danyo que hace


effect eEff = EffectDamage(iDam);//definimos el efecto danyo

ApplyEffectToObject(DURATION_TYPE_INSTANT, eEff, oPC);//y se lo aplicamos al PJ, sufreeee!!!!
}
Así queda más claro, y además como ya dije cuando vimos las variables, así podemos usar las variables que hemos definido por si quisiéramos hacer más cosas. Ahora imaginad que queremos aplicar un efecto visual de sangre sobre el mismo PJ. En vez de buscarlo otra vez con GetEnteringObject(), usamos oPC. Y queremos que tarde 1 segundo, así que usamos la función DelayCommand (os toca buscarla, ¡que me canso!).
 
void main()
{
object oPC = GetEnteringObject();//quien entra

int iDam = d6()+5; //danyo que hace


effect eEff = EffectDamage(iDam);//definimos el efecto danyo

ApplyEffectToObject(DURATION_TYPE_INSTANT, eEff, oPC);//y se lo aplicamos al PJ, sufreeee!!!!

effect eVis = EffectVisualEffect(VFX_COM_BLOOD_SPARK_MEDIUM); //efecto visual de sangre
DelayCommand(1.0, ApplyEffectToObject(DURATION_TYPE_INSTANT, eVis, oPC));//aplicado al segundo 1.0
}
 
Importante fijarse también, aparte de que hemos podido utilizar el oPC gracias a que lo hemos definido, en dos cosas más:

1) Al poner el DelayCommand, como hemos tenido que poner un (, hemos cerrado con otro ). ¡Siempre se cierran! Y después de cada función recordad, va el ;. En este caso vemos que la función del Apply la hemos convertido en parámetro de otra.

2) Esto es solo un consejo, no una obligación. Yo he llamado a las variables oPC, iDam, eEff y eVis; como podría haberlas llamado de otra forma (recordad a oPC Paquito de unas lecciones atrás xD). Por experiencia propia, os diré que lo mejor es poner nombres sencillos, y que sepan decirte qué has definido con este nombre. Y más que el nombre, la primera letra no es coincidencia.

oPC es un object
iDan es un int
eEff es un efecto
eVis es otro efecto


¡Y ya está! ¡Ya hemos hecho el script! Tranquilos que otro día lo complicaremos :)

Editado por Setaka, 25 June 2015 - 09:49 PM.

Tutorial NWN Scripting: Click aquí


#8 Setaka

Setaka

    Ancillae

  • Miembro
  • PipPipPip
  • 362 posts

Posteado 26 June 2015 - 12:03 AM

6. CONDICIONALES (if, else, else if, switch)
 
Los condicionales sirven para ejecutar distintas líneas de código según se cumpla o no la condición que nosotros definimos, de forma que podemos dirigir la lectura de código según nuestros intereses.
 
La estructura más simple es la de if:

if(se cumple esta condición)
{
ejecuta este código
}
código que seguirá leyendo haya cumplido o no la condición 

 
Imaginemos que queremos que la trampa mágica que hemos creado solamente afecte a los PJs, para que no dañe a los monstruos que pongamos en el dungeon. Usaremos como condición la función GetIsPC, que en la descripción pone que devuelve TRUE si la criatura es un PJ, FALSE si no.
 

void main()
{
object oPC = GetEnteringObject();//quien entra

if(GetIsPC(oPC)==TRUE)
{
int iDam = d6()+5; //danyo que hace
effect eEff = EffectDamage(iDam);//definimos el efecto danyo
ApplyEffectToObject(DURATION_TYPE_INSTANT, eEff, oPC);//y se lo aplicamos al PJ, sufreeee!!!!
effect eVis = EffectVisualEffect(VFX_COM_BLOOD_SPARK_MEDIUM); //efecto visual de sangre
DelayCommand(1.0, ApplyEffectToObject(DURATION_TYPE_INSTANT, eVis, oPC));//aplicado al segundo 1.0
}
}

Entonces solo provocará daño a las criaturas que cumplan la condición que hemos puesto. Si quisiéramos lo contrario, que solo dañará a quien no sea un PJ, pondríamos FALSE.
 
Importante darse cuenta que hemos usado ==, esto sirve para decirle que queremos que nos compare los dos valores que hay a los dos lados de los símbolos para ver si son iguales. No confundir, = es para definir variables, creando una igualdad a ambos lados del símbolo, == para establecer una comparación de igualdad. En este caso GetIsPC(oPC) nos devuelve TRUE y nosotros hemos preguntado si el valor que devuelve es también TRUE.

 

Habiendo visto que ==, ahora toca presentar a !=. Si con == preguntamos si los dos valores son iguales, != sirve para ver si son distintos. Así pues...

 

if(GetIsPC(oPC)==TRUE)

 

es lo mismo que poner...

 

if(GetIsPC(oPC)!=FALSE)

 

... aunque complicándolo más xD

 

 

Otra forma de escribirlo sería sin poner ni TRUE ni FALSE.

if(GetIsPC(oPC))
{
Ejecuta esto si es PJ, es lo mismo que si pusiéramos == TRUE
} 

O, muy utilizado, poner el ! delante de la función

if(!GetIsPC(oPC))
{
Ejecuta esto si NO es PJ, es lo mismo que si pusiéramos == FALSE o !=TRUE
} 

 

También veréis que en muchos scripts usan return;. Esto se utiliza para detener el script hasta donde ha leído. Así pues, también podríamos poner, si no queremos que se haga nada de nada con las criaturas no PJs que activen la trampa...

void main()
{
object oPC = GetEnteringObject();//quien entra

if(!GetIsPC(oPC)){return;} //si no es PJ, deja de leer aquí

//entonces todo lo siguiente, lo leerá para quien sea PJ
int iDam = d6()+5; //danyo que hace
effect eEff = EffectDamage(iDam);//definimos el efecto danyo
ApplyEffectToObject(DURATION_TYPE_INSTANT, eEff, oPC);//y se lo aplicamos al PJ, sufreeee!!!!
effect eVis = EffectVisualEffect(VFX_COM_BLOOD_SPARK_MEDIUM); //efecto visual de sangre
DelayCommand(1.0, ApplyEffectToObject(DURATION_TYPE_INSTANT, eVis, oPC));//aplicado al segundo 1.0

}

 

Pero, ¿y si queremos por ejemplo que a los PJs los dañe, y a los no PJs los cure? Conociendo el if, podríamos hacer la estructura:

if(GetIsPC(oPC)
{
daña
}
if(!GetIsPC(oPC)
{
cura
} 

 

O el método correcto, usando else, que traducido sería "si no", para todas las opciones que no cumplan la condición:

if(condición)
{
haz esto
}
else
{
haz esto otro
} 

Y quedaría algo cómo:

void main()
{
object oPC = GetEnteringObject();//quien entra
effect eEff; //definimos variable... pero aun no sabemos que es
int iDam = d6()+5; //danyo o curación que hace
if(GetIsPC(oPC)) //si es PJ...
{
eEff = EffectDamage(iDam);//eEff es un efecto de daño
effect eVis = EffectVisualEffect(VFX_COM_BLOOD_SPARK_MEDIUM); //efecto visual de sangre
DelayCommand(1.0, ApplyEffectToObject(DURATION_TYPE_INSTANT, eVis, oPC));//aplicado al segundo 1.0
}
else //... y si no...
{
eEff = EffectHeal(iDam);//eEff es un efecto de curacion
}

ApplyEffectToObject(DURATION_TYPE_INSTANT, eEff, oPC);//y aplicamos eEff al PJ o bicho
}

Fíjaos en el truqui utilizado. Esta vez el int iDam lo he dejado fuera de la estructura condicional. De esta forma está definido sea o no sea PJ, así que uso el mismo valor aleatorio iDam tanto si quiero dañar o curar. Después eEff no tiene inicialmente asignado qué es, si daño o curación... depende de cómo lea el script será una cosa u otra. Finalmente una vez lo hemos decidido, aplicamos este efecto. Podríamos haberlo separado todo, repitiendo el ApplyEffect y crear un nuevo valor aleatorio para curar, pero cómo ya he ido diciendo: a cuantas menos líneas mejor.

 

 

 

Otra estructura es con la utilización del else if (traducción: "y si no"), para ir descartando.

if(condición)
{
haz esto
}
else if(otra condición)
{
haz esto otro
}
else if(oootra condición)
{
o esto
}
else
{
si no cumple ninguna, haz esto
}
 

 

Otros símbolos aparte de != y == son <, >, >= y <= para comparar valores y que cumplan la condición de ser...

 

... más pequeños <

... más pequeño o igual <=

... más grandes >

... más grande o igual >=

 

 

Y cuando combinemos condiciones, && y || para decir y o o

if( condición1 && condición2 )
{
si se cumplen las dos
}

if( condición1 || condición2 )
{
si se cumple CUALQUIERA de las dos
}
 

 

 

 

Por ejemplo, haciendo un ejemplo conjunto de varias cosas que hemos visto (operaciones, else if y estos símbolos). Si quisiéramos que el daño que se aplica al jugador dependa de la edad del mismo, haríamos...

 

[...]
int iDam = d6()+5; //valor base
int iAge = GetAge(oPC); //edad que tiene el PJ

if( (iAge>14)&&(iAge<=25) ) //si tiene entre 15 y 25 años
{
iDam=iDam*2; //dañamos el doble, los jóvenes pueden soportarlo
}
else if( (iAge>25)&&(iAge<=80) )//Entre 26 y 80
{
iDam=iDam/2; //ya están viejales, la mitad de daño 
}
else
{
iDam=1; //El resto, solo un toque de atención... que no están pa muchas aventuras
}

effect eEff = EffectDamage(iDam);//definimos el efecto daño
ApplyEffectToObject(DURATION_TYPE_INSTANT, eEff, oPC);//y se lo aplicamos al PJ, sufreeee!!!!

 

 

Este último else recoge los valores sobrantes, que serían los PJs de menos de 15 (no incluido) y los mayores de 81 (incluido)... que sería más fácil que poner:

else if( (iAge<15) || (iAge>=81) )

 

También cuidado, si dividimos por 2 puede quedar decimal (float) y tenemos definido int. En este caso se redondeará el valor.

 

Y otra estructura más de condición... ¡la última, lo juro! :P es la estructura switch (traducción: alterna) Realmente se puede suplir con la anterior de if, else if y else, pero no está de más conocerla por si os la encontráis en algún script, saber qué hace.

 

La estructura es:

switch(valor)
{
si es x: haz esto; para;
si es y: haz esto; para;
si es z: haz esto; para;
y si no cumple ninguno: haz esto; para;
} 

 

For example... Queremos que el PJ que caiga en la trampa diga 1 de 4 posibles frases aleatorias, y que no siempre diga algo.

 

Dentro de la condición de si es PJ (no queremos que lo digan los bichos) podríamos poner:

int i12 = d12(); //tira un dado de 12
int sFrase; //declaramos la variable, de momento es nula y le asignaremos valor según el resultado del d12

switch(i12) 
{
case 1: sFrase = "¡Cachis!"; break; //si la tirada es 1
case 2: sFrase = "Cagontó"; break; //si la tirada es 2
case 3: sFrase = "¡Maldición!"; break; //si la tirada es 3
case 4: sFrase = "Brbrbrbrb"; break; //si la tirada es 4
default: break; //de 5 a 12, sFrase no es nada... lo que significa que sFrase = ""
}

if(sFrase!="") //así que ponemos la condición de que si sFrase ha sido definida...
{
AssignCommand(oPC, SpeakString(sFrase)); //... el PJ la diga
}

Nota: Cómo la función SpeakString no tiene cómo parámetro a quién se aplica, se usa el AssignCommand para decirle que es el PJ quien queremos que lo diga. De no hacerlo, cómo el script está en el evento OnEnter de un desencadenante, estaríamos haciendo que lo "dijera" el desencadenante.


Editado por Setaka, 02 July 2015 - 01:08 PM.

Tutorial NWN Scripting: Click aquí


#9 Setaka

Setaka

    Ancillae

  • Miembro
  • PipPipPip
  • 362 posts

Posteado 04 July 2015 - 03:11 PM

7. BUCLES (while, for, do)

Este tema está pensado para enseñar el uso de bucles o loops. Esto es, repetir código un número determinado de veces o hacer recorridos.

La estructura del bucle más común se utiliza con while (traducción: mientras)
 

while(se cumple esta condición)
{
haz esto
}

Lo más importante cuando creemos esta estructura es hacer que se pueda salir de ella (que deje de cumplir la condición que establecemos). De no hacerla se crea un bucle infinito y, aunque pueda compilar bien en el editor, en el juego se mostraría un mensaje de error ("nombre del script", demasiadas instrucciones). Un ejemplo correcto de loop sería:
 

int iNum; //una variable contador

while(iNum<5) //mientras sea menor de 5...
{
//acciones ... realizamos las acciones que queramos
iNum++; //y sumamos +1
}

//entonces solo saldrá del bucle cuando sea mayor o igual de 5

Por ejemplo si pusiéramos un efecto de daño, dañaría al objetivo las veces que estableciéramos en la condición, en vez de tener que copiar/pegar la misma función 5 veces.
 
El mayor uso que se le da a un loop es hacer recorridos. Para ver las funciones disponibles que realizan recorridos podéis usar el buscador de funciones, ya sea usando la palabra First (primer objeto del recorrido) cómo Next (nos buscará el siguiente).
 
Por ejemplo, uno muy usado es el GetFirstPC(). Esta función devuelve el primer PJ de la lista de jugadores interna del juego. En un módulo singleplayer detectaría al único jugador, pero en multiplayer devolvería un jugador cualquiera, y para obtener los demás deberíamos hacer el recorrido con GetNextPC().
 
Un ejemplo práctico, si queremos hacer una función para que cualquier jugador pueda saber una vez dentro del juego cuantos DMs hay conectados bien para molestarles o bien para enviarles infinitas muestras de amor para que le otorgue el preciado y totalmente merecido desbloqueo, podríamos hacer un recorrido de la lista de jugadores, y después ir determinando si cada jugador es un DM o no para hacer un cálculo final del total de DMs conectados..

int iDM; //variable contador de DMs, por defecto 0

object oPC = GetFirstPC(); //primer jugador, la función incluye PJs y DMs

while(GetIsObjectValid(oPC)) //mientras oPC siga siendo un objeto válido...
{

if( (GetIsDM(oPC)) || (GetIsDMPossessed(oPC)) ) //si es un DM (o criatura controlada por DM)
{
iDM++; //aumentamos la variable contador en 1
}

oPC = GetNextPC(); //IMPORTANTE. oPC es ahora el siguiente PJ de la lista, nos aseguramos que saldremos del loop cuando no queden más jugadores a analizar
}

string sDM = IntToString(iDM); //una vez hecho el recorrido y tenemos un total de iDM, lo pasamos a texto
SendMessageToPC(oJugador, "Hay "+sDM+" DM's conectados"); //y completamos y enviamos el mensaje (ej "Hay 3 DM's conectados")

 Cuando no hay más jugadores en la lista, GetNextPC no encontrará a ninguno y devolverá el valor OBJECT_INVALID, saliendo del loop porque no cumplirá la condición inicial.


 
En general, hacer un recorrido siempre debería ahorrarte líneas de código. Volviendo al ejemplo del script de la trampa mágica, si quisiéramos que el daño dependiera también de cuántos orbes mágicos hay a su alrededor, en vez de ir haciendo un script distinto a cada desencadenante poniendo nosotros manualmente el valor de los orbes, podríamos usar el loop para calcular cuantos de estos ubicados especiales existen; pudiendo así usar el mismo script pero con efecto distinto a cada trampa.
 

object oPC = GetEnteringObject();
int iNum; //contador de orbes mágicos, por defecto 0

object oTarget = GetFirstObjectInShape(SHAPE_SPHERE, 15.0, oPC, TRUE, OBJECT_TYPE_PLACEABLE); //detectamos el primero de los ubicados no estáticos a 15.0 de distancia esférica del oPC que lo pisa

while(GetIsObjectValid(oTarget)) //mientras exista un ubicado que aun no ha pasado por el recorrido...
{
if(GetTag(oTarget)=="orbe_magico") //si el ubicado detectado tiene de etiqueta "orbe_magico"...
{
iNum++; //contador +1
}
oTarget = GetNextObjectInShape(SHAPE_SPHERE, 15.0, oPC, TRUE, OBJECT_TYPE_PLACEABLE); //IMPORTANTE. Detectamos el siguiente de los ubicados no estáticos a 15.0 de distancia del oPC que lo pisa
}

iDanyo = iDanyo*iNum; //una vez fuera del bucle y calculado iNum, aplicamos daño según su valor

 
Es muy importante ver que aunque nos ahorremos líneas y podemos incrementar el rendimiento óptimo de un script, si no vigilamos podría suceder lo contrario. Esto es, en recorridos demasiado pesados. El while ejecuta todo en milisegundos, y si hacemos bucles cómo recorridos de todos los objetos del inventario de un PJ, o de todos los objetos de un área, esto consume muchos recursos del servidor y contribuye al lag. Siempre que sea posible, es preferible filtrar el tipo de objeto a buscar, cómo en el script anterior, que he filtrado que solo buscara de entre los ubicados, descartando así criaturas, puertas y demás. Nota para los mappers: marcad los ubicados que solo tengan finalidad estética cómo estáticos, que después se incrementa notablemente el rendimiento ingame  :thumb:
 
 
Otras estructuras para hacer un loop son for, y do, que siempre está bien conocer la estructura por si los encontráis en otros scripts, pero mientras dominéis el while ya se puede cumplir su función perfectamente. Dejo los links para que echéis un vistazo:

 

for: http://www.nwnlexico...?title=For_Loop

do: http://www.nwnlexico...p?title=Do_Loop


Editado por Setaka, 04 July 2015 - 03:15 PM.

Tutorial NWN Scripting: Click aquí


#10 Setaka

Setaka

    Ancillae

  • Miembro
  • PipPipPip
  • 362 posts

Posteado 18 July 2015 - 07:25 PM

8. VARIABLES LOCALES

Cómo ya hemos visto, en cualquier script podemos definir variables para que nos almacenen información para usarla más adelante en el mismo.
 

object oPC = GetFirstPC(); //oPC nos guarda quién es el PJ
int iAge = GetAge(oPC); //iAge nos guarda los años que tiene oPC
string sName = GetName(oPC); //sName su nombre
location lLoc = GetLocation(oPC); //lLoc su localización en el módulo
float fFace = GetFacing(oPC); //sFace su orientación

 
En el código anterior he definido unas variables que podría usar más adelante en el script, pero una vez se terminara, perdería la información que contienen. Las variables locales, en cambio, permiten guardar información para recuperarla en cualquier momento posterior. Esta información se guarda siempre en un object del módulo. Para visualizarlo fácilmente, es cómo si pudiéramos pegar post-its en los objetos del módulo, cada uno con un nombre distinto de título y con información en cada uno.
 
Las funciones que nos permiten guardar la información siempre empiezan por SetLocal, seguido del tipo de variable que queremos guardar. SetLocalInt, SetLocalFloat, SetLocalString, etc. Todas siguen la misma estructura que la siguiente:

void SetLocalInt(object oObject, string sVarName, int nValue); 

En este caso es para guardar una variable tipo int.
- Primero definimos el oObject, esto es, el objeto que nos guardará la información (a quién pegamos el post-it).
- Seguidamente el sVarName. El nombre de la variable local, siempre es texto. Esto sería el título del post-it, para diferenciarlo de los demás que puede haber pegados.
- El valor de la variable. La información que queremos guardar (el contenido del post-it). Tiene que coincidir con la función que utilizamos. Si usamos la función SetLocalString, evidentemente el valor del parámetro sería también tipo string.
 
 
Guardamos ahora la información que hemos recuperado. En este caso lo guardo en el objeto módulo, de la misma forma que podría guardarlo en un objeto área. ¡No solo se puede almacenar variables en criaturas!
 

object oPC = GetFirstPC(); //oPC nos guarda quién es el PJ
int iAge = GetAge(oPC); //iAge nos guarda los años que tiene oPC
string sName = GetName(oPC); //sName su nombre
location lLoc = GetLocation(oPC); //lLoc su localización en el módulo
float fFace = GetFacing(oPC); //sFace su orientación

object oMod = GetModule(); //el objeto módulo

SetLocalInt(oMod, "iAge", iAge); //guardamos valor entero
SetLocalString(oMod, "sName", sName);//texto
SetLocalLocation(oMod, "sLoc", sLoc);//localización
SetLocalFloat(oMod, "fFace", fFace);//valor decimal
SetLocalObject(oMod, "oPC", oPC); //guardamos un object

Si os fijáis, he puesto el mismo nombre de variable que el que ya tenía (puesto entre "" para convertirlo en texto). De todas formas, se puede poner cómo se quiera siempre que no se cree un caos con los nombres. Puedo poner "sName" cómo haber puesto "Nombre", y "iAge" cómo "Edad". Lo importante es no liarse, para que cuando tengamos que recuperar la información, no nos equivoquemos de nombre.
 
Ahora que tenemos las variables guardadas, podemos recuperarlas, aunque sea en un script distinto y al cabo de mucho tiempo. Si para guardar las funciones seguían el patrón SetLocal(tipo), para recuperarlas es GetLocal(tipo).
 

object oMod = GetModule(); //nuestro módulo

int iAge = GetLocalInt(oMod, "iAge");
string sName = GetLocalString(oMod, "sName");
location lLoc = GetLocalInt(oMod, "lLoc");
float fFace = GetLocalFloat(oMod, "fFace");
object oPC = GetLocalObject(oMod, "oPC");

//y ahora podemos usar la información contenida
AssignCommand(oPC, SpeakString("Me llamo "+sName+" y tengo "+IntToString(iAge)+" años"));

 
Ahora bien, espero que hayáis podido apreciar que cómo efecto práctico el ejemplo anterior no sirve de nada xD, puesto que es una información que podemos sacar directamente de un PJ con la misma función que hemos utilizado para guardar las variables, pero espero que haya quedado claro su uso.
 
Al igual que podemos crear variables, una vez dejan de servirnos podemos eliminarlas para dejar el sistema a 0. La función sigue la estructura DeleteLocal(tipo), poniendo el object donde se ha almacenado y nombre de la variable a borrar.
 
 
También podemos asignar variables directamente desde el toolset, haciendo clic secundario en un objeto y seleccionando "variables". De esta forma almacenamos información que podemos utilizar en los scripts ya dentro del juego. Esto es perfecto para crear scripts genéricos, que son scripts que cumplen una función pero con resultado distinto según hayamos configurado.
 
Veremos ahora un simple script genérico que nos servirá para que cuando usemos un ubicado, nos teletransporte donde hayamos configurado con una variable. Podemos usarlo para ubicados puerta (en vez de dibujar la mini-transición fea delante de la puerta), o incluso portales.
 

void main()
{
object oSelf = OBJECT_SELF; //objeto donde está el evento
object oPC = GetLastUsedBy(); //quien lo usa
string sDestino = GetLocalString(oSelf, "sDestino");//variable local sDestino
object oWP = GetWaypointByTag(sDestino);//punto de ruta con etiqueta = al valor sDestino
AssignCommand(oPC, ClearAllActions(TRUE));//paramos acciones del PJ por si acaso
AssignCommand(oPC, ActionJumpToObject(oWP, FALSE)); //y teletransportamos
}

Tan solo es necesario definir una variable en el ubicado, de tipo string y nombre sDestino, y colocar un punto de ruta en cualquier lugar del módulo, de etiqueta igual al valor de la variable. Con esto ya nos podemos ahorrar un montón de scripts extra si tuviéramos que crear un script para cada ubicado puerta o portal.
 
Avanzado: Con imaginación podemos crear muchas más cosas manteniéndolo genérico: si necesita llave, si queremos un efecto visual (número del efecto en el visualeffects.2da), tiempo de retraso entre usarlo y que teletransporte (estilo portales).

Spoiler

 

 

También muchas veces lo importante no es el contenido de una variable, sino si existe o no, de forma que podemos ejecutar un condicional que tenga resultados distintos. Si queremos hacer un desencadenante que al pisarlo envíe al PJ un texto descriptivo, podríamos, en vez de hacer un script distinto a cada texto que queremos hacer, asignarlo cómo variable al desencadenante. Y si queremos que solo lo envíe una vez por PJ, asignarle a este PJ una variable que indique si ya lo ha pisado o no.

void main()
{
object oPC = GetEnteringObject(); //objeto que entra
if(!GetIsPC(oPC)){return;} //Si no es PJ, no seguimos

object oSelf = OBJECT_SELF; //objeto desencadenante donde esta el evento
string sTag = GetTag(oSelf); //su etiqueta

if(GetLocalInt(oPC, sTag)){return;} //si el PJ ya ha entrado una vez... no seguimos
SetLocalInt(oPC, sTag, 1); //y si no, lo marcamos cómo que ya ha entrado

string sTexto = GetLocalString(oSelf, "sTexto"); //variable sTexto
FloatingTextStringOnCreature(sTexto, oPC); //texto flotante
} 

En este sistema tenemos dos variables locales:

- La variable sTexto, en el desencadenante, la asignaríamos desde el toolset, y pondríamos el mensaje que querríamos que se mostrara.

- La variable, de nombre igual al de la etiqueta del desencadenante, la asignaríamos al PJ que entra en él. Aquí no nos importa la información que tenga, solo si existe o no. Si no existe en el PJ, la creamos y ejecutamos el código. Si existe en el PJ, significa que ya hemos ejecutado el código y no tenemos que hacerlo otra vez.

 

De querer hacer que volviera a estar activo para este PJ al cabo de unos segundos, podríamos hacer un DelayCommand con el borrado de la variable en el PJ.

 

Avanzado: Realiza una tirada secreta y muestra un texto al PJ si la pasa. Definimos en el desencadenante las variables de la habilidad que hace tirada (número en el skills.2da), la CD que queremos, y el texto a mostrar.

Spoiler

 

 

Las variables locales solo se guardarán hasta que se reinicie el servidor... excepto las variables que se guardan en los items de los PJs. Estas son permanentes. Un sistema clásico para almacenar variables persistentes es darle al PJ recién creado un item del que nunca se desprenderá, para poder ir almacenando datos persistentes en este, por ejemplo las quests estáticas, para que además de no poder repetirlas, saber en qué punto se encuentra de cada una de ellas. Un sistema avanzado para almacenar variables de forma persistente es el uso de tablas MySQL utilizando el NWNX (link al tutorial de configuración). También existe la opción de guardar variables persistentes con el sistema por defecto del juego, SetCampaign(tipo), pero no resulta una buena opción salvo contados casos debido a que es muy difícil de administrar.

 

Extendido por lavafuego: Uso de base de datos por defecto del juego.

Spoiler

 

 

Extra: Recomiendo usar script genéricos siempre que se pueda, de esta forma se evita código redundante y hacen el módulo más ágil a la par que es más fácil localizar scripts en él. Muchas veces no hace ni falta utilizar variables para configurar un script genérico, ya que la misma etiqueta de un objeto ya nos está almacenando información que podríamos utilizar.

Spoiler

Editado por Setaka, 27 July 2015 - 12:17 AM.

Tutorial NWN Scripting: Click aquí


#11 Setaka

Setaka

    Ancillae

  • Miembro
  • PipPipPip
  • 362 posts

Posteado 21 July 2015 - 12:08 PM

9. FUNCIONES PROPIAS (by lavafuego)
 
Ya tienes unos conocimientos básicos de script y seguramente en algún momento necesites crear alguna función debido a que la que necesitas no se encuentra entre las funciones que nos dejaron los creadores del juego.


Por ejemplo (por poner uno…) si nos fijamos en la función:

void DelayCommand(float fSeconds, action aActionToDelay) 

 

Imaginemos que queremos crear un objeto pasados 1 segundo. Nos fijamos en la función de crear objeto:

object CreateObject(int nObjectType, string sTemplate, location lLocation, int bUseAppearAnimation=FALSE, string sNewTag="") 

 

Cómo veis un delay es incompatible con ella… ya que crear un objeto nos devuelve un object y el delay requiere un action no un object. La solución pasaría por:

 

- Crear un nuevo script que contenga la línea de crear objeto, y usar en el primer script un Delay con un ExecutScript para arrancar el segundo... lo que nos implica que tendríamos que hacer un segundo script nuevo cada vez que quisiéramos usarlo... :(

 

- O crear una función propia en el mismo script, y usar el delay con ella :)

 

Voy a intentar explicar cómo se crea una función, como reclamarlas etc.

Lo primero es tener claro el valor que queremos que nos retorne la función, si queremos un object, un int, un void…

En nuestro caso va a ser un void para poder ejecutarlo con el delay, después, tenemos que darle un nombre.
Luego como toda función paréntesis (que es donde incluiremos los parámetros que deseemos, ya lo explico más adelante) y entre corchetes desarrollamos nuestra función.
 

En nuestro caso es esta:

void CrearUnObjeto()
{
string sObjetoACrear="x3_plc_brazier";// placeable brasero
object oPuntoDeRuta=GetObjectByTag("WP_brasero"); //punto de ruta donde queremos crearlo
location lPuntoDeRuta=GetLocation(oPuntoDeRuta); //obtenemos su localización, ya que la siguiente función nos la pide
CreateObject(OBJECT_TYPE_PLACEABLE, sObjetoACrear, lPuntoDeRuta); //lo creamos
} 

 

Parece sencillo… ahora ¿qué hacemos con esto?. Pues bien una forma de reclamarla es poner nuestra función al inicio del script, antes del void main y más tarde usar esa función dentro del script (después del void main)


Crear un desencadenante genérico y un punto de ruta con tag WP_brasero, y en el onEnter del desencadenante poner:

//Nuestra función, la definimos antes del void main para que podamos usarla ahí
//De hacerlo bien (¡clic en guardar!) se mostrará nuestra nueva función en la lista de funciones de la derecha, marcada en negrita para indicar que es propia
//Si lo estáis visualizando en el editor, también os saldrán estos comentarios que estoy escribiendo, ya que sirve para añadir información a
//las funciones que creamos y así no olvidarnos de qué diantres hacía :D
void CrearUnObjeto()
{
string sObjetoACrear="x3_plc_brazier";// placeable brasero
object oPuntoDeRuta=GetObjectByTag("WP_brasero"); //punto de ruta donde queremos crearlo
location lPuntoDeRuta=GetLocation(oPuntoDeRuta); //obtenemos su localización, ya que la siguiente función nos la pide
CreateObject(OBJECT_TYPE_PLACEABLE, sObjetoACrear, lPuntoDeRuta); //lo creamos
} 

void main()
{
//Ya con nuestra función void creada, solo nos queda llamarla con el Delay
DelayCommand(1.0f, CrearUnObjeto());//reclamamos la función
}
 

Veis creamos la función antes del void main y la usamos después.



Otra forma es crear la función y guardarla como un script normal, independiente. Para reclamarla en otros script futuros que hagamos (que difícil xD).
¿Cómo hacemos esto?
Primero cogemos nuestra función (no el void solo la función CrearUnObjeto()) y la guardamos como un script más y le ponemos un nombre. Luego cuando la necesitemos en un script futuro reclamarla. Esto es parecido pero distinto al método :( antes mencionado. En el método :( lo que hacíamos era crear otro void main específico, aquí creamos un script sin void main, donde ponemos la función creada y podemos llamarla desde otros scripts que queramos, junto con otras funciones que creemos (creando una librería de funciones, que aparecerían en negrita en la pestaña de funciones).

 

¿Pero cómo se hace eso?


Sencillo hay una formula llamada #include que es poner al inicio del script y antes del void main: almoadilla include espacio y entre comillas el nombre del script que queremos incluir.
De esta forma es como si pusiéramos el script completo delante del void main, como hicimos antes arriba.
 

La forma escritura es esta:

#include “nombre_de_nuestro_script”

Paso a paso, con nuestro ejemplo para que se entienda mejor toda esta parrafada.

Guardamos esta función con el nombre “crear”:

void CrearUnObjeto()
{
string sObjetoACrear="x3_plc_brazier";// placeable brasero
object oPuntoDeRuta=GetObjectByTag("WP_brasero");
location lPuntoDeRuta=GetLocation(oPuntoDeRuta);
CreateObject(OBJECT_TYPE_PLACEABLE, sObjetoACrear, lPuntoDeRuta);
} 

 

Y en el OnEnter...

#include "crear"
void main()
{
DelayCommand(1.0f, CrearUnObjeto());//reclamamos la función
} 

 


Cómo veis es muy parecido a lo que hicimos antes solo que al separar nuestra función en un script independiente, podemos reclamarla en todos los script que queramos. No solo en este, simplemente con un include. Cada vez que pongamos delante de un void main #include "crear" es como si tuviéramos la función entera, juntamente con otras que podríamos haber dejado en el mismo script  ;) 

Si pusiéramos la función delante del void main escrita entera con corchetes y todo, como en el primer ejemplo, solo sirve para ese script en concreto. Pero si la guardamos como un script más y la reclamamos con un include la podemos poner en muchos scripts. ¡Pero muy importante! Los scripts con include, para que funcionen correctamente, hay que tenerlos ambos actualizados y sincronizados. ¡Siempre que haya cambios hay que guardar los dos scripts, el del include y el incluido!

No sé si ha quedado clara la cosa, cualquier cosa a Setaka que es mas ducho explicando xD (nota de Setaka: pero yo os cobraré así que... ¡¡¡a por él!!!)


 

Ahora vamos a dar un pasito más.

Resulta que nos ha gustado la función void CrearUnObjeto(), pero queremos cambiar en cada script lo que vamos a crear (unas veces será un brasero, otras una estatua etc). En la función CreateObject, mediante un string ponemos el tag del objeto que queremos crear, por eso en nuestra función (la CrearUnObjeto), vamos a dejar sin definir ese string para definirlo en el script dónde reclamemos nuestra función, solamente vamos a darle nombre a la variable sin más. Y más tarde en el script que la reclamemos la definimos por ejemplo.

Un brasero:
string sObjetoACrear="x3_plc_brazier";
Una estatua de gárgola:
string sObjetoACrear="NW_STAT_GARG";
etc, lo que queramos

Vamos con otro ejemplo para que quede más claro.

Nos vamos a la función original y donde antes estaban los paréntesis, ponemos lo que queremos definir en el script futuro en nuestro caso el string. Quedando así:

 

void CrearUnObjeto(string sObjetoACrear)
//damos nombre al string pero no se define mas adelante
{
object oPuntoDeRuta=GetObjectByTag("WP_brasero");
location lPuntoDeRuta=GetLocation(oPuntoDeRuta);
CreateObject(OBJECT_TYPE_PLACEABLE, sObjetoACrear, lPuntoDeRuta);
//sObjetoACrear, se reclama pero no esta definido
} 

 

Os habéis fijado, string sObjetoACrear dentro de los paréntesis, y después en CreateObject lo reclamamos con sObjetoACrear y aún no está definida la variable, eso se hace en el script donde reclamemos la función. Lo que hemos hecho ahora es añadirle un parámetro, que tendremos que definir cuando usemos las funciones dentro de los void main.

Volviendo al ejemplo del desencadenante para que quede más claro, ahora queremos crear una estatua gárgola, cuyo tag es "NW_STAT_GARG", la definiríamos en el script.
Vamos al desencadenante y en el OnEnter la función quedaría así:

#include "crear"
void main()
{
string sObjetoACrear="NW_STAT_GARG";
DelayCommand(1.0f, CrearUnObjeto(sObjetoACrear));
} 

 

Veis hemos definido el string sObjetoACrear en el script dónde mediante el include reclamamos la función CrearUnObjeto
string sObjetoACrear="NW_STAT_GARG";

fijaros que al ser la función así:
void CrearUnObjeto(string sObjetoACrear)
cuando usamos la función en un script hay que poner dentro de los paréntesis el string.
Mirad el delay:
DelayCommand(1.0f, CrearUnObjeto(sObjetoACrear));


Y así con todo lo que queramos definir en el futuro en un script, por ejemplo el objeto punto de ruta, porque no va a ser el mismo en todos los script que reclamemos la función. Entonces añadimos ooootro parámetro más:

void CrearUnObjeto(string sObjetoACrear,object oPuntoDeRuta)
{
location lPuntoDeRuta=GetLocation(oPuntoDeRuta);
CreateObject(OBJECT_TYPE_PLACEABLE, sObjetoACrear, lPuntoDeRuta);
} 

 

Dentro del paréntesis variable string sObjetoACrear y la variable object oPuntoDeRuta. Están presentes en la función CreateObject, pero que definiremos en el script dónde reclamemos con el include nuestra función no aquí.

La cosa que cuando reclamamos la función en el desencadenante quedaría ahora así:

 

#include "crear"
void main()
{
string sObjetoACrear="NW_STAT_GARG";
object oPuntoDeRuta=GetObjectByTag("WP_Gargola");
DelayCommand(1.0f, CrearUnObjeto(sObjetoACrear,oPuntoDeRuta));//reclamamos la función
} 

 

Probad cambiando el objeto a crear en varios script y el punto de ruta y varios desencadenantes, veréis como funciona   ;)

 

 

Avanzado: Predefinir función, funciones propias retroalimentadas (estilo heartbeat), parámetros por defecto, funciones no void. By Setaka.

Spoiler


Editado por Setaka, 21 July 2015 - 01:23 PM.

Tutorial NWN Scripting: Click aquí


#12 Setaka

Setaka

    Ancillae

  • Miembro
  • PipPipPip
  • 362 posts

Posteado 09 August 2015 - 11:22 PM

10. CONVERSACIONES
 
En este tema se tratará solamente los scripts relacionados con las conversaciones, no cómo crear los nodos, enlaces, etiquetas, asignar sonidos y demás; por lo que es algo que se dará por sabido.
 
 Empezaremos con algunas funciones básicas que se usan en las conversaciones, en los scripts que se asignan en el editor de conversaciones:

object oPC = GetPCSpeaker(); //PJ que está hablando con el NPC
object oNPC = OBJECT_SELF; //Cómo siempre, objeto desde el que arranca el evento
object oLast = GetLastSpeaker(); //obtenemos último PJ que habló con él

Y la siguiente función, bien puede utilizarse dentro de un script asignado a una conversación (ej: de una conversación saltamos a otra), o bien en otro del módulo (ej: empieza una conversación al clicar un ubicado).

void ActionStartConversation(
    object oObjectToConverseWith,
    string sDialogResRef = "",
    int bPrivateConversation = FALSE,
    int bPlayHello = TRUE
);

 
En un archivo de conversación hay tres eventos importantes:
 
El texto aparece cuando...: Un script que establece una condición en la que determinamos la línea que dice el PNJ, o las opciones de respuesta disponibles para el PJ. El script se ejecuta ANTES de que se muestre la línea.
Acciones emprendidas: DESPUÉS de haber mostrado la línea de diálogo, ejecuta este script. En el caso de respuesta del NPC, cuando se puede leer la línea, en el caso del PJ, cuando la selecciona por respuesta.
Interrumpido: Si la conversación se termina y no es por haber clicado una línea de diálogo marcada con [FINALIZAR DIÁLOGO]. Si es una conversación de un sistema, es perfecto para borrar estas variables que nos dejan el sistema a 0 de nuevo.
 
Los dos últimos no tienen secreto, es un típico script void main() cómo todos los que hemos visto, tan solo se tiene que definir correctamente el PJ y el NPC. Nos centramos en el evento condicional, el texto aparece cuando.
 
Cuando creemos un script nuevo abriéndolo desde esta pestaña nos encontraremos que no empieza por void, sino que se trata de una función int StartingConditional. Entonces tenemos que establecer qué valor devuelve esta función y en qué condición. Si devuelve TRUE, se mostrará la línea en la que se asignó el script, si devuelve FALSE, no se mostrará.
 
Entonces si quisiéramos comprobar si el PJ tiene 300 de oro para que le salga una línea de conversación donde un vendedor le diera un objeto a cambio, haríamos algo cómo:
 
- ¿Le interesa comprarme estas setas?
            - Sí, aquí tienes 300 de oro. [el texto aparece cuando: asignamos script para que aparezca esta línea SOLO si tiene 300 de oro o más]
            - Ahora mismo no me interesa. [ningún script asignado, sale siempre]
 
El script podría ser algo cómo:

int StartingConditional()
{
object oPC = GetPCSpeaker(); //el PJ
int iOro = GetGold(oPC); //el oro que tiene

if(iOro>=300) //si tiene 300 o más...
{
return TRUE;//... devolvemos TRUE. Muestra la línea y termina el script
}
return FALSE; //Por defecto: No la muestra
}

Entonces así solo mostrará la línea si tiene dicha cantidad, pero aún faltaría quitarle el oro. Aquí mucho cuidado, la tendencia suele ser poner en la misma línea, en acciones emprendidas, que quite el oro y dé el objeto, pero se está cometiendo un grave bug. Cómo he recalcado, uno se ejecuta antes de mostrar la línea, el de acciones emprendidas después. Entonces quitaría el oro cuando el PJ clicara en la línea de "Si, aquí tienes el oro". Peeeeero... pueden haber pasado 5 minutos desde que salió la línea y el PJ clique en ella... tiempo más que de sobras para que el jugador haya dejado en el suelo todo el oro, de modo que cuando clicara en la línea, no tendría realmente el oro.
 
La solución pasaría por hacer otro condicional dentro del script de acciones emprendidas, para comprobar justo antes de entregar el objeto que aún tiene el oro, o bien el método que queda mejor, seguir usando las líneas de conversación y sus eventos, esta vez una condición en la línea de respuesta del NPC cuando hemos seleccionado la opción de darle el oro.
 
- Sí, aquí tienes 300 de oro. [el texto aparece cuando: asignamos script para que aparezca esta línea SOLO si tiene 300 de oro o más]
          - Gracias por su compra. [el texto aparece cuando: asignamos script para que aparezca esta línea SOLO si tiene 300 de oro o más] [acc emprendidas: quitar oro, dar objeto]
          - Emhhh... No vale gitanear...  [opción por defecto siempre que la anterior línea no se muestre]
 
Podemos utilizar el mismo condicional tanto para la respuesta del PJ como para la del vendedor. Y como el condicional y la acción emprendida de un PNJ se ejecutan simultáneamente, nos aseguramos de que el PJ no se ha desprendido del oro. También si se quiere se puede quitar el condicional en la respuesta del PJ, de manera que puedan ofrecer 300 de oro aunque no los tengan, y después la respuesta condicional del PJ ya se encargaría de filtrar.
 
Fijaros en un detalle: los condicionales de respuesta del PJ se muestran todos los que definamos. Es decir que mientras la línea devuelva TRUE, aparecerá. En cambio en la respuesta del NPC sólo saldrá la primera línea (en el orden de arriba abajo) que no sea FALSE. En este caso anterior, si no se muestra el texto "Gracias por su compra", la opción por defecto es que no le da el objeto ni le quita el oro. ¡El NPC no dirá dos líneas a la vez!
 
Otra utilidad de los condicionales es hacer la típica conversación de una sola línea que sale al azar:
 
- Vaya vaya... ¡Hola! [el texto aparece cuando d4()==1]
- Tienes pinta de aventurero... ¡Aléjate de mi! [el texto aparece cuando d4()==1]
- ¿Si? [el texto aparece cuando d4()==1]
- De noche, ten cuidado con tu bolsa de oro... ...¿Dónde dices que la guardas?... [ningún condicional asignado, saldrá si ninguna ha salido antes]
 
Extra: Conversaciones de una sola línea aleatoria sin archivo de conversación.

Spoiler
 
 
Aprendido lo anterior ya se podría crear la mayoría de conversaciones de un módulo, pero faltaría explicar algo más avanzado, que es el uso de etiquetas personalizadas para crear conversaciones con texto más dinámico.
 
Lo que nombro como "etiquetas personalizadas" son los custom tokens, que sería igual que las etiquetas que vienen por defecto en la conversación (ej: <FullName>), pero nosotros definimos qué texto mostrará. Estas etiquetas son globales en todo el módulo, lo que significa que no son específicas para cada PJ. Por lo tanto, si queremos utilizarlo en una conversación, debemos actualizarlo a cada vez para que sí sea específico para el mismo.
 
Si quisiéramos que el mercader informara del oro que tiene el PJ cuando no acepta el trueque, nos encontramos que no tenemos la etiqueta y tendríamos que crearla. Queremos obtener la información justo antes de mostrar la línea para que el texto no esté desactualizado (ni muestre la información de otro PJ), así que tocará recopilar la información desde el evento el texto aparece cuando.

int StartingConditional()
{
object oPC = GetPCSpeaker(); //el PJ
int iOro = GetGold(oPC); //el oro que tiene
string sOro = IntToString(iOro); //pasamos a texto

SetCustomToken(1337, sOro); //creamos el custom con etiqueta 1337. Guarda la info de sOro

return TRUE; //Por defecto: SIEMPRE aparece la línea, en este caso no necesitamos el FALSE en ninguna parte
}

Hemos creado la etiqueta. A diferencia de las genéricas del juego, se asigna un número en vez de un nombre. En este caso he utilizado el 1337. Se puede usar cualquiera excepto de 0 a 9 porque los utiliza el juego. Es importante saber cuales tenemos utilizados en el módulo, y que se actualicen correctamente
 
Después, en la conversación de la respuesta del mercader cuando el PJ no tiene el oro, pondríamos:
 
- Gracias por su compra. [el texto aparece cuando: asignamos script para que aparezca esta línea SOLO si tiene 300 de oro o más] [acc emprendidas: quitar oro, dar objeto]
- Emhhh... No vale gitanear... ¡Solo tienes <CUSTOM1337> de oro!  [condición: script anterior, por defecto siempre que la anterior línea no se muestre]
 
 
Con estas etiquetas personalizadas podemos crear sistemas por conversación con mayor facilidad de uso. Por ejemplo en una herramienta DM puede mostrar qué criatura ha seleccionado, o podemos mostrar el nombre de un item que hemos seleccionado para cambiarle el nombre, mostrar información de los PJs y DMs conectados, mostrar información de variables locales almacenadas... todo lo que sea texto, tiene cabida. Incluso aunque sean párrafos grandes. Y si se quiere crear por script un salto de línea (bien para mostrar texto en el log, bien para hablarlo directo o bien para archivo de conversación), añadiendo \n al texto.

- ¿Qué quieres hacer?
          - Obtener lista de jugadores
                     - <CUSTOM6666> [condición: siempre TRUE; solo usamos el evento para actualizar el texto al momento]
                              - ¿Qué quieres hacer?

int StartingConditional()
{
object oPC = GetFirstPC(); //primer jugador de la lista
string sText; //texto a mostrar, aún por crear

while(oPC!=OBJECT_INVALID) //mientras oPC sea valido
{
sText += GetName(oPC)+"\n"; //obtenemos su nombre, lo sumamos al texto ya creado, y salto de línea
oPC = GetNextPC(); //¡Que pase el siguiente jugador!
}

SetCustomToken(6666, sText); //grabamos el sText final

return TRUE; //Por defecto: Siempre sale la línea
}

Editado por Setaka, 09 August 2015 - 11:23 PM.

Tutorial NWN Scripting: Click aquí





A Bragol. Tus amigos te echan de menos.