Friday, June 14, 2019

Mientras tanto en Plutón... Índice

Aquí les dejo el índice de los artículos acerca de los archivos de configuración del motor Enki que he estado escupiendo los últimos meses para más fácil acceso y digestión.
  1. Cómo hemos cambiado. Introducción y localizaciones.
  2. Hágase la luz. Fuentes de luz y su relación con las descripciones.
  3. Por donde he venido, me voy. Movimiento.
  4. Esta ni siquiera es mi forma final. Descripción de atributos adicionales de localizaciones.
  5. Banderas de nuestros parsers. Definición de flags.
  6. Objetos orientados a objetos. Definición básica de objetos.
  7. Esta es CASI mi forma final. Descripción de atributos de objetos.
  8. Trigger warning. Disparadores y efectos.
  9. Más diversión con localizaciones. Disparadores relacionados con localizaciones.
  10. Sí, empezamos por el medio. Definición de datos de la aventura.
  11. ¿Es a mí?. Personajes no jugadores.
  12. Bla bla bla. Conversaciones.
  13. Cuenta conmigo. Contadores.
  14. Pro ludo mori. Muerte de personajes y finalización de la aventura.
  15. Trigger warning 2: The triggering. Disparadores globales.

Thursday, June 13, 2019

Mientras tanto, en Plutón...

En la entrada de hoy vamos a hablar de nuevo de disparadores, así que antes de empezar les recomiendo que le den un repaso al artículo en el que hablamos sobre ellos.

Trigger warning 2: The triggering

A lo largo de estos artículos hemos visto que hay unos cuantos lugares en los que encontraremos la definición de disparadores, pero hasta ahora no hemos visto disparadores independientes que existan sin tener una relación directa con un objeto, localización, contador o personaje.

Encontraremos este tipo de disparador "independiente" en el archivo triggers.json. Usaremos parte del archivo perteneciente a Pluto Crash para que nos sirva de ejemplo.

[
  {
    "id":1,
    "triggerType":9,
    "triggerSubType":1,
    "beenTriggered":"false",
    "actionId":null,
    "locationId":null,
    "itemId":16,
    "characterId":null,
    "enabled":"true",
    "effects":[
      {
        "id":1,
        "type":6,
        "passiveGameItems":[19],
        "passiveGameLocations":null,
        "passiveGameNPCs":null,
        "passiveGameTriggers":null,
        "passiveGameFlags":null,
        "newIntegerValue":null,
        "newBooleanValue":null,
        "newStringValue":null,
        "message":null
      },
      {
        "id":2,
        "type":401,
        "passiveGameItems":null,
        "passiveGameLocations":null,
        "passiveGameNPCs":null,
        "passiveGameTriggers":null,
        "passiveGameFlags":[9],
        "newIntegerValue":null,
        "newBooleanValue":true,
        "newStringValue":null,
        "message":null
      },
      {
        "id":3,
        "type":401,
        "passiveGameItems":null,
        "passiveGameLocations":null,
        "passiveGameNPCs":null,
        "passiveGameTriggers":null,
        "passiveGameFlags":[11],
        "newIntegerValue":null,
        "newBooleanValue":false,
        "newStringValue":null,
        "message":null
      }
    ]
  },
 ...

]

El disparador que vemos aquí se activará al introducir la llave de seguridad en su ranura en el panel de control de apertura de la puerta de acceso de mercancías. Si han jugado a Pluto Crash recordarán que esto se lleva a cabo durante una secuencia que rompe el flujo habitual de la aventura en la que, en lugar de la pantalla normal de juego, pasamos a una representación a pantalla completa de dicho panel.
Es en este tipo de secuencias interactivas -sí, es su nombre oficial- desde donde se hará uso de los disparadores independientes, ya que no se está llevando a cabo ninguna de las acciones que habitualmente provocan la activación de un disparador, pero requerimos de su activación y de sus efectos. Simplemente se solicitará a la actividad principal que se debe ejecutar un disparador mientras le proporcionamos el identificador numérico del mismo y ésta buscará el disparador correspondiente para comprobar si su activación es posible como haría con cualquier otro disparador.

Y hasta aquí los artículos dedicados a los archivos de configuración. En las próximas entradas entraremos más en la chicha del motor para ver cómo son interpretados para dar forma al conjunto de puzzles absurdos y muertes idiotas que son la esencia vital de toda aventura conversacional.

¡Ale, hasta la próxima!

Wednesday, June 12, 2019

Mientras tanto, en Plutón...

Al finalizar la entrada anterior habíamos comentado que hoy aprenderíamos a matar a un personaje.
Por desgracia para los más truculentos no será esto un festival de tripas virtuales.

Pro ludo mori

Para empezar, hay que diferenciar entre los dos tipos posibles de personajes que podemos mandar al otro barrio: los personajes no jugadores (o PNJ) y el personaje que maneja el jugador.

Para "matar" a un PNJ tendremos que activar el efecto EFFECT_DESTROY_NPC. A efectos prácticos lo que se hará es asignar al atributo location del personaje el valor 0. Como ninguna localización debe tener el identificador 0 esto sitúa al personaje fuera del alcance del jugador eliminándolo del juego.
La definición del efecto que llevaría a cabo la tarea sería como sigue:

{
    "id":1,
    "type":8,
    "passiveGameItems":null,
    "passiveGameLocations":null,
    "passiveGameNPCs":[1],
    "passiveGameTriggers":null,
    "passiveGameFlags":null,
    "newIntegerValue":null,
    "newBooleanValue":null,
    "newStringValue":null,
    "message":null
}

Siendo el valor 8 del atributo type el correspondiente al efecto EFFECT_DESTROY_NPC y el valor 1 de passiveGameNPCs el identificador del personaje que queremos eliminar.

Por si se lo preguntaban, sí, este mismo resultado puede conseguirse mediante un efecto que cambie la localización del PNJ especificando el valor 0 como valor de la nueva localización. Este efecto se definiría así:

{
    "id":1,
    "type":12,
    "passiveGameItems":null,
    "passiveGameLocations":null,
    "passiveGameNPCs":[1],
    "passiveGameTriggers":null,
    "passiveGameFlags":null,
    "newIntegerValue":0,
    "newBooleanValue":null,
    "newStringValue":null,
    "message":null
}

Siendo 12 el valor del efecto EFFECT_TELEPORT_NPC. Aunque, por supuesto, mi recomendación personal es utilizar el efecto correcto para cada situación conviene tener en cuenta que este método puede utilizarse para devolver a un PNJ al mundo de los vivos.

Por su lado, matar al personaje jugador requiere, previsiblemente,  algo más de preparación.

Para empezar se debe tener en cuenta que en Enki, morir y ganar son la misma cosa. El resultado de matar al personaje y llegar al final del juego es el mismo: mostrar la pantalla definida en la actividad DeathScreen.java. Sí, probablemente debería haber usado un nombre más neutro pero a estas alturas ya me da pereza cambiarlo.

La pantalla DeathScreen.java mostrará la información correspondiente a la muerte -o victoria- del personaje de acuerdo al contenido del archivo death_infos.json.

[
  {
    "id":-1,
    "headerText":"La has palmado",
    "bodyText":"La has palmado finamente por bajar donde no debías.",
    "image":"death_001.png"
  }
]


El contenido de este archivo se cargan en un array de objetos DeathInfo.java, cuyos atributos son los siguientes.
  • id. Identificador numérico único de la pantalla.
  • headerText. Texto que se mostrará en la cabecera de la pantalla.
  • bodyText. Texto principal que se mostrará en la pantalla.
  • image. Nombre del archivo de imagen que se mostrará en la pantalla.
Al realizar la llamada a DeathScreen.java se le pasará el objeto DeathInfo correspondiente y se mostrará la información adecuada.


El primer y más simple método para llegar a esta pantalla es mediante una orden de movimiento que desemboque en la muerte del personaje. Como ya habíamos mencionado al hablar del movimiento, si la dirección hacia la que se mueve tiene un valor destination menor que cero, dicho movimiento provocará la muerte del personaje. El motor buscará en el array de objetos DeathInfo aquel que tenga un id igual al valor de destination y llamará a DeathScreen, pasándole la información adecuada.

El segundo método para matar al personaje jugador es mediante un efecto, concretamente el efecto EFFECT_DEATH.

{
    "id":1,
    "type":1003,
    "passiveGameItems":null,
    "passiveGameLocations":null,
    "passiveGameNPCs":null,
    "passiveGameTriggers":null,
    "passiveGameFlags":null,
    "newIntegerValue":-2,
    "newBooleanValue":null,
    "newStringValue":null,
    "message":null
}


Siendo 1003 el valor de EFFECT_DEATH. Al ejecutar este efecto, se buscará el objeto DeathInfo con el valor de id especificado en newIntegerValue y se pasará como argumento a DeathScreen del mismo modo que con la muerte por movimiento.

Y esto ha sido todo por hoy, permanezcan atentos porque con la próxima entrada terminaremos con los archivos de configuración y podremos pasar a otras cosas.
¡Ale, a matar no, que está feo!

Monday, June 10, 2019

Mientras tanto, en Plutón...

En la entrada de hoy veremos la definición de contadores, una herramienta que nos permitirá simular, entre otras cosas, el paso del tiempo durante la partida.
Ánimo, que ya queda menos.

Cuenta conmigo

Los contadores se definen el en archivo counters.json y son implementados en la clase Counter.java.

Usaremos de nuevo Pluto Crash como ejemplo. Si no lo han jugado o no han llegado al final... ¡ALERTA SPOILERS!

Si lo han terminado entonces sabrán que para llegar al final es necesario poner una bomba casera en el motor de plasma de la nave invasora y escapar de la base antes de que explote. Esta cuenta atrás se lleva a cabo mediante un contador definido como sigue.

[
  {
    "id":1,
    "name":"bomb counter",
    "counterType":2,
    "initialValue":14,
    "currentValue":14,
    "limitValue":0,
    "onStartTriggers":null,
    "onCancelTriggers":null,
    "onLimitReachedTriggers":[
      {
        "id":1,
        "triggerType":0,
        "triggerSubType":2,
        "beenTriggered":"false",
        "actionId":null,
        "locationId":null,
        "itemId":null,
        "characterId":null,
        "enabled":"true",
        "effects":[
          {
            "id":1,
            "type":1003,
            "passiveGameItems":null,
            "passiveGameLocations":null,
            "passiveGameNPCs":null,
            "passiveGameTriggers":null,
            "passiveGameFlags":null,
            "newIntegerValue":-5,
            "newBooleanValue":null,
            "newStringValue":null,
            "message":null
          }
        ],
        "conditionalFlags":[
          {
            "id":6,
            "name":"Fuera base",
            "type":1,
            "booleanValue":"true",
            "integerValue":null
          },
          {
            "id":7,
            "name":"K128 conectado",
            "type":1,
            "booleanValue":"true",
            "integerValue":null
          }
        ],
        "activeItemConditions":null,
        "passiveItemConditions":null
      },
      {
        "id":2,
        "triggerType":0,
        "triggerSubType":2,
        "beenTriggered":"false",
        "actionId":null,
        "locationId":null,
        "itemId":null,
        "characterId":null,
        "enabled":"true",
        "effects":[
          {
            "id":1,
            "type":1003,
            "passiveGameItems":null,
            "passiveGameLocations":null,
            "passiveGameNPCs":null,
            "passiveGameTriggers":null,
            "passiveGameFlags":null,
            "newIntegerValue":-6,
            "newBooleanValue":null,
            "newStringValue":null,
            "message":null
          }
        ],
        "conditionalFlags":[
          {
            "id":6,
            "name":"Fuera base",
            "type":1,
            "booleanValue":"true",
            "integerValue":null
          },
          {
            "id":7,
            "name":"K128 conectado",
            "type":1,
            "booleanValue":"false",
            "integerValue":null
          }
        ],
        "activeItemConditions":null,
        "passiveItemConditions":null
      },
      {
        "id":3,
        "triggerType":0,
        "triggerSubType":2,
        "beenTriggered":"false",
        "actionId":null,
        "locationId":null,
        "itemId":null,
        "characterId":null,
        "enabled":"true",
        "effects":[
          {
            "id":1,
            "type":1003,
            "passiveGameItems":null,
            "passiveGameLocations":null,
            "passiveGameNPCs":null,
            "passiveGameTriggers":null,
            "passiveGameFlags":null,
            "newIntegerValue":-7,
            "newBooleanValue":null,
            "newStringValue":null,
            "message":null
          }
        ],
        "conditionalFlags":[
          {
            "id":6,
            "name":"Fuera base",
            "type":1,
            "booleanValue":"false",
            "integerValue":null
          },
          {
            "id":7,
            "name":"K128 conectado",
            "type":1,
            "booleanValue":"true",
            "integerValue":null
          }
        ],
        "activeItemConditions":null,
        "passiveItemConditions":null
      },
      {
        "id":4,
        "triggerType":0,
        "triggerSubType":2,
        "beenTriggered":"false",
        "actionId":null,
        "locationId":null,
        "itemId":null,
        "characterId":null,
        "enabled":"true",
        "effects":[
          {
            "id":1,
            "type":1003,
            "passiveGameItems":null,
            "passiveGameLocations":null,
            "passiveGameNPCs":null,
            "passiveGameTriggers":null,
            "passiveGameFlags":null,
            "newIntegerValue":-8,
            "newBooleanValue":null,
            "newStringValue":null,
            "message":null
          }
        ],
        "conditionalFlags":[
          {
            "id":6,
            "name":"Fuera base",
            "type":1,
            "booleanValue":"false",
            "integerValue":null
          },
          {
            "id":7,
            "name":"K128 conectado",
            "type":1,
            "booleanValue":"false",
            "integerValue":null
          }
        ],
        "activeItemConditions":null,
        "passiveItemConditions":null
      }
    ],
    "automaticCounter":true,
    "enabled":false
  },
  ...
]


Los atributos de los que se compone la clase son los siguientes.
  • id. Identificador numérico único del contador. 
  • name. Nombre del contador, usado sólo para mejorar la legibilidad de la definición de los contadores. 
  • counterType. Tipo de contador. Los contadores pueden pertenecer a uno de los cuatro tipos definidos -COUNTER_REGULAR_INCREASE, COUNTER_REGULAR_DECREASE, COUNTER_INDEFINITE_INCREASE y COUNTER_INDEFINITE_DECREASE- en función de su aumentan o disminuyen su valor y si tienen un valor límite definido. 
  • initialValue. Valor inicial del contador. 
  • currentValue. Valor actual del contador. 
  • limitValue. Valor límite al que puede llegar el contador en caso de ser de tipo COUNTER_REGULAR_INCREASE o COUNTER_REGULAR_DECREASE. Al llegar a este valor se activarán los disparadores definidos en el atributo correspondiente. 
  • onStartTriggers, onCancelTriggers.Pendientes de implementación, contendrán los disparadores que se activarán al iniciar o cancelar el contador. 
  • onLimitReachedTriggers. Array de disparadores que se activarán cuando currentValue tenga el mismo valor que limitValue.
  • automaticCounter. Atributo booleano que indica si el contador debe actualizarse de manera automática durante la secuencia de ejecución del turno. Un contador no automático deberá actualizarse mediante efectos.
  • enabled. Atributo booleano que indica si el contador está habilitado.
El funcionamiento de un contador automático está relacionado con el proceso de ejecución del turno.

Cuando el jugador solicita la ejecución de una acción -como moverse o usar un objeto- se inicia la secuencia de ejecución del turno. El segundo paso de este proceso es la actualización de los contadores automáticos -siendo la primera el análisis de la orden introducida por el jugador, por ahora no profundizaremos en esto- mediante la llamada al método pingCounter( ) definido en la clase Counter.java.
Este método actualizará el atributo currentValue del contador y lo comparará con el valor de limitValue. Si son iguales, devolverá el valor true, de modo que desde el proceso de ejecución del turno se sabrá que se deben activar los disparadores definidos en onLimitReachedTriggers.

Hay que tener en cuenta, sin embargo , que cierto tipo de acciones consideradas "rápidas" -examinar un objeto o personaje, solicitar el listado de salidas e inventario y la acción "mirar"- harán que el proceso de ejecución del turno se salte este paso, por lo que no actualizarán los contadores automáticos.

Y esto es todo por hoy, en la próxima entrada aprenderemos a matar a un personaje.
Ale, a contar.

Friday, June 07, 2019

Mientras tanto, en Plutón...

Ya vimos en la entrada anterior cómo se definen los personajes con los que se encontrará el jugador a lo largo de la aventura, así que profundizaremos ahora en lo que le da el nombre a las aventuras conversacionales. Efectivamente, las conversaciones.

Bla bla bla

Para empezar, vamos a echarle un vistazo a la definición de un árbol de conversación -y de los elementos que dependen de él- en el archivo conversation_trees.json. Usaremos uno de los árboles de Pluto Crash como ejemplo.

[
    {
        "id":1,
        "conversationBranches":[
    {
        "id":1,
        "conversationText":"La señal roja de alerta parpadea en la pantalla-cara de K128 mientras aúlla un constante '!ALERTA, PELIGRO!' a través de sus altavoces. Es bastante molesto.",
        "triggers":null,
        "conversationOptions":[
            {
        "id":0,
        "optionText":"Vale, vale, alerta peligro, lo pillo ¿Se puede saber qué ha pasado?",
        "destinationBranch":2,
        "moodRequirements":null,
        "locationRequirements":null,
        "inventoryRequirements":null,
        "skillRequirements":null,
        "gameTriggers":null
            }
        ],
        "conversationImage":"k128_dialog_warning.png"
    },
    ...
    {
        "id":8,
        "conversationText":"'Aquí tienes una lista de los daños que he podido comprobar:\n*Comunicaciones externas fuera de línea.\n*Control de soporte vital planta suelo inoperativo.\n*Control de soporte vital plantas -1 y -2 operativo.\n*Ascensor en línea. Inoperativo.\n*Robot de cocina y preparador de alimentos inoperativo.\n*Cubas de reciclaje operativas.\n*Laboratorio hidropónico operativo.\n*Arsenal y control automatizado de seguridad fuera de línea.\n*Enfermería operativa.'",
        "triggers":null,
        "conversationOptions":[
            {
        "id":0,
        "optionText":"¿Qué ocurre con las comunicaciones?",
        "destinationBranch":9,
        "moodRequirements":null,
        "locationRequirements":null,
        "inventoryRequirements":null,
        "skillRequirements":null,
        "gameTriggers":null
            },
            {
        "id":1,
        "optionText":"¿Qué hay del soporte vital?",
        "destinationBranch":10,
        "moodRequirements":null,
        "locationRequirements":null,
        "inventoryRequirements":null,
        "skillRequirements":null,
        "gameTriggers":null
            },
            {
        "id":2,
        "optionText":"¿Ascensor en línea pero inoperativo? Explícate.",
        "destinationBranch":11,
        "moodRequirements":null,
        "locationRequirements":null,
        "inventoryRequirements":null,
        "skillRequirements":null,
        "gameTriggers":null
            },
            {
        "id":3,
        "optionText":"¿Qué ocurre con la cocina?",
        "destinationBranch":12,
        "moodRequirements":null,
        "locationRequirements":null,
        "inventoryRequirements":null,
        "skillRequirements":null,
        "gameTriggers":null
            },
            {
        "id":4,
        "optionText":"¿Qué hay de las cubas de reciclaje?",
        "destinationBranch":13,
        "moodRequirements":null,
        "locationRequirements":null,
        "inventoryRequirements":null,
        "skillRequirements":null,
        "gameTriggers":null
            },
            {
        "id":5,
        "optionText":"¿Algún problema con el laboratorio hidropónico?",
        "destinationBranch":14,
        "moodRequirements":null,
        "locationRequirements":null,
        "inventoryRequirements":null,
        "skillRequirements":null,
        "gameTriggers":null
            },
            {
        "id":6,
        "optionText":"¿Han reventado el arsenal?¡Venga ya!",
        "destinationBranch":15,
        "moodRequirements":null,
        "locationRequirements":null,
        "inventoryRequirements":null,
        "skillRequirements":null,
        "gameTriggers":null
            },
            {
        "id":7,
        "optionText":"¿Qué hay de la enfermería?",
        "destinationBranch":16,
        "moodRequirements":null,
        "locationRequirements":null,
        "inventoryRequirements":null,
        "skillRequirements":null,
        "gameTriggers":null
            },
            {
        "id":8,
        "optionText":"Supongo que podría haber sido peor.",
        "destinationBranch":17,
        "moodRequirements":null,
        "locationRequirements":null,
        "inventoryRequirements":null,
        "skillRequirements":null,
        "gameTriggers":null
            }
        ],
        "conversationImage":"k128_dialog_serious.png"
    },
    ...
   
    {
        "id":18,
        "conversationText":"Giras la cabeza de K128 hasta que ésta se separa de su brazo robótico y la colocas bajo tu brazo.",
        "triggers":null,
        "conversationOptions":[
            {
        "id":0,
        "optionText":"¡La aventura nos aguarda!",
        "destinationBranch":0,
        "moodRequirements":null,
        "locationRequirements":null,
        "inventoryRequirements":null,
        "skillRequirements":null,
        "gameTriggers":[
            {
                "id":1,
                "triggerType":0,
                "triggerSubType":0,
                "beenTriggered":"false",
                "actionId":null,
                "locationId":null,
                "itemId":null,
                "characterId":null,
                "enabled":"true",
                "effects":[
            {
                "id":1,
                "type":110,
                "passiveGameItems":null,
                "passiveGameLocations":null,
                "passiveGameNPCs":[1],
                "passiveGameTriggers":null,
                "passiveGameFlags":null,
                "newIntegerValue":1,
                "newBooleanValue":null,
                "newStringValue":null,
                "message":null
            },{
                "id":2,
                "type":106,
                "passiveGameItems":null,
                "passiveGameLocations":null,
                "passiveGameNPCs":[1],
                "passiveGameTriggers":null,
                "passiveGameFlags":null,
                "newIntegerValue":4,
                "newBooleanValue":null,
                "newStringValue":null,
                "message":null
            },{
                "id":3,
                "type":111,
                "passiveGameItems":null,
                "passiveGameLocations":null,
                "passiveGameNPCs":[1],
                "passiveGameTriggers":null,
                "passiveGameFlags":null,
                "newIntegerValue":null,
                "newBooleanValue":"false",
                "newStringValue":null,
                "message":null
            },{
                "id":4,
                "type":109,
                "passiveGameItems":null,
                "passiveGameLocations":null,
                "passiveGameNPCs":[1],
                "passiveGameTriggers":null,
                "passiveGameFlags":null,
                "newIntegerValue":2,
                "newBooleanValue":null,
                "newStringValue":null,
                "message":null
            },{
                "id":5,
                "type":113,
                "passiveGameItems":null,
                "passiveGameLocations":null,
                "passiveGameNPCs":[1],
                "passiveGameTriggers":null,
                "passiveGameFlags":null,
                "newIntegerValue":null,
                "newBooleanValue":null,
                "newStringValue":"",
                "message":null
            },{
                "id":6,
                "type":401,
                "passiveGameItems":null,
                "passiveGameLocations":null,
                "passiveGameNPCs":null,
                "passiveGameTriggers":null,
                "passiveGameFlags":[8],
                "newIntegerValue":null,
                "newBooleanValue":true,
                "newStringValue":null,
                "message":null
            }
                ]
            }
        ]
            }
        ],
        "conversationImage":"k128_dialog.png"
    }
        ],
        "initialBranch":1,
        "moodRequirements":null,
        "locationRequirements":null,
        "inventoryRequirements":null,
        "defaultImage":"k128_dialog.png",
        "triggers":null
    },
...
]

Como se puede ver -más o menos- la conversaciones se apoyan en tres elementos, los árboles de conversación -implementados en la clase ConversationTree.java-, las ramas de conversación -ConversationBranch.java- y las opciones de conversación -ConversationOption.java-. Un árbol estará compuesto de varias ramas que presentan al jugador la parte del diálogo que corresponde al personaje con el que está hablando. Estas ramas tienen asociadas una serie de opciones que representan las respuestas que dará el jugador y que determinan la navegación a través de las diferentes ramas del árbol.

Orrarrum

Los diferentes atributos asociados a un árbol de conversación son los siguientes.
  • id. El identificador numérico único asociado al árbol de conversación.
  • conversationBranches. Un array de objetos ConversationBranch.java que contiene todas las ramas de conversación asociadas al árbol.
  • initialBranch. La rama inicial del árbol. Al iniciar una conversación, el texto y la imagen asociadas a esta rama, así como sus opciones de ocnversación, serán las que sean presentadas al jugador.
  • moodRequirements, locationRequirements, inventoryRequirements. Pendientes de implementación. En un futuro servirán para hacer que el acceso a un árbol de conversación esté condicionado por la actitud del personaje, la localización en la que se encuentra y los objetos que el jugador lleve en su inventario.
  • defaultImage. Imagen por defecto de la conversación. Será la imagen que se muestre en la pantalla de conversación si la rama actual no define ninguna imagen específica.
  • triggers. Disparadores que se activaran al iniciar o finalizar la conversación. Los disparadores definidos con tipo ON_TALK_TRIGGER se activarán al empezar la conversación mientras los definidos como ON_CONVERSATION_FINISH_TRIGGER se activarán cuando el jugador seleccione una opción de conversación que dé por finalizada la misma.
Por su parte las ramas tienen los siguientes atributos.
  • id. Como siempre, el identificador numérico de la rama.conversationText. El texto que se mostrará en pantalla al entrar en la rama de conversación.
  • triggers. Pendiente de implementación. Contendría los disparadores que se activarían al llegar a la rama; probablemente se elimine este campo debido a que, a efectos prácticos, esto ya se consigue con los disparadores definidos en la clase ConversationOption.java.
  • conversationOptions. Un array de objetos ConversationOption.java con las respuestas disponibles.
  • conversationImage. La imagen que se deberá mostrar en la pantalla del diálogo, en caso de que sea diferente de la imagen por defecto de la conversación.
Al entrar en una rama se mostrará en pantalla el texto indicado en conversationText y un listado con las opciones definidas en conversationOptions. El jugador podrá seleccionar una de estas opciones, lo que hará que se cargue otra rama del árbol o se termine la conversación volviendo a la pantalla de juego normal.

Por su parte, los atributos de las opciones de conversación son los siguientes.
  • id. Sí, el identificador numérico de la opción de conversación.
  • optionText. El texto que se mostrará en la lista de opciones de conversación.
  • destinationBranch. El identificador numérico de la rama de la conversación que se deberá mostrar al seleccionar la opción. Si el valor es 0 se finalizará la conversación.
  • moodRequirements, locationRequirements, inventoryRequirements, skillRequirements. Pendientes de implementación. De manera similar a los campos análogos del árbol de conversación, representan requisitos relativos a la actitud y ubicación del personaje, a objetos del inventario del jugador y a valores en sus habilidades (este último aspecto no está implementado, ya hablaremos de él cuando veamos la definición de los datos del personaje jugador).
  • gameTriggers. Disparadores que se activarán al seleccionar la opción de conversación.
Pues por hoy creo que tienen bastante que masticar. No se preocupen que cada vez queda menos para terminar con los archivos de configuración y entrar a cosas más divertidas. Más o menos.

Ale, a conversar.

Thursday, June 06, 2019

Trigger warning

Igual debería estar buscando trabajo en vez de andar haciendo estas chorradas. Aunque teniendo en cuenta lo que me río cada vez que lo veo tal vez lo que debería hacer es pedir una minusvalía, o algo.


Wednesday, May 29, 2019

Mientras tanto, en Plutón...

Qué ¿Ya pensaban que había abandonado esto? Pues no, lo que pasa es que he estado entretenido trabajando en el editor de árboles de conversación y, claro, no se puede estar en misa y repicando.

Y, hablando de árboles de conversación, deberíamos echarle un vistazo a los mismos. Pero por una vez vamos a ir por orden...

¿Es a mí?

Como resulta evidente, empezar a hablar de los árboles de conversación no tendría mucho sentido sin hablar antes acerca de los personajes a los que están asociados.

Los personajes son probablemente el aspecto que más ha evolucionado desde la versión primitiva del motor, en la que se consideraban objetos con un comportamiento un poco diferente. Los personajes ahora se definen en el archivo npcs.json, que tiene este aspecto -usaremos de nuevo Pluto Crash como ejemplo-:

[
  {
    "id":1,
    "names":["K128"],
    "location":3,
    "onLocationDescription":"El asistente robótico K128 te observa desde el techo, una señal de peligro parpadea en la pantalla que le sirve de cara.",
    "examineDescription":"Un asistente robótico modelo K128, no es mucho más que una cabeza enganchada a un brazo que se mueve por raíles. Hace un tiempo estaban por todas partes, ahora es casi una pieza de museo.",
    "characterListDescription":"K128 es el asistente robótico de la base. En teoría su cometido es ayudarte con el mantenimiento pero siendo poco más que una cabeza parlante no hace mucho más que dar conversación.\nPeor sería estar completamente solo, eso sí.",
    "onMeetTriggers":null,
    "onLeaveTriggers":null,
    "onActionTriggers":null,
    "onExamineTriggers":null,
    "behavior":0,
    "movementLocations":null,
    "mood":1,
    "conversationTrees":[1],
    "singleStringConversation":"El puerto de comunicaciones de K128 está conectado a la antena para enviar el mensaje de auxilio, no es buena idea desconectarlo ahora.",
    "visibility":1,
    "met":"false",
    "allowedOnCharacterList":"true",
    "fast":"false",
    "smallImage":"k128_s.png",
    "bigImage":"k128.png",
    "showOnScene":"true"
  },
  ...
]


Los diferentes atributos de la clase GameNPC.java cumplen las siguientes funciones:
  • id. El identificador numérico único del personaje.
  • names. Los posibles nombres que el jugador puede usar a la hora de interactuar con el personaje a través de la introducción de instrucciones. Funcionan de manera similar a los nombres de los objetos, siendo el primero que aparece el nombre por defecto.
  • location.Localización en la que se encuentra el personaje.
  • onLocationDescription. Texto que se añadirá a la descripción de la localización cuando el personaje se encuentre en la misma.
  • examineDescription, characterListDescription. El primer valor se muestra al examinar el personaje, el segundo valor es opcional y muestra una descripción más detallada si se está usando la opción de mostrar los personajes examinados desde el panel de objetos a pantalla completa.
  • onMeetTriggers, onLeaveTriggers, onActionTriggers, onExamineTriggers. Disparadores que se activarán al realizar determinadas acciones relacionadas con el personaje. Pendiente de implementación.
  • behavior. Comportamiento del personaje. En el ejemplo mostrado el valor 0 indica que el personaje es estático y no se moverá de la localización en la que se encuentra. Otros valores harán que el personaje siga al jugador, patrulle una serie de localizaciones, se mueva a una localización específica siguiendo un camino determinado o aparezca en localizaciones aleatorias dentro de un conjunto determinado.
  • movementLocations. Localizaciones por las que se moverá el personaje. En el ejemplo mostrado, al ser un personaje estático no se especifican localizaciones de movimiento; si el personaje tuviese un comportamiento diferente se debería indicar aquí un array con los identificadores de las localizaciones por las que se movería el personaje.
  • mood. Actitud del personaje hacia el jugador. Si la actitud del personaje es lo suficientemente hostil, atacará al jugador si tiene oportunidad.
  • conversationTrees. Árboles de conversación asignados al personaje. Cuando el jugador introduzca la orden de hablar con el personaje el árbol de conversación adecuado se escogerá de entre los árboles referenciados en este array.
  • singleStringConversation. Si el jugador introduce la orden de hablar con un personaje que no tiene árboles de conversación definidos en su lugar se mostrará sólo el texto que se especifique en este atributo.
  • visibility. Condiciones de luz necesarias para poder ver -e interactuar- con el personaje. Funciona de la misma manera que la visibilidad de objetos.
  • met. Indica si el jugador ha hablado ya con el personaje. Este atributo se usará principalmente para construir la lista de personajes que aparecerán en la lista de personajes conocidos en el panel de personajes.
  • allowedOnCharacterList. Valor booleano que indica si se puede mostrar al personaje en la lista del panel de personajes.
  • fast. Valor booleano que indica si el personaje es más rápido que el jugador. Los personajes rápidos actúan antes que el jugador cuando éste lleva a cabo una acción.
  • smallImage. Imagen que se mostrará en el listado de personajes en el panel correspondiente.
  • bigImage. Imagen que se mostrará sobre la imagen de la localización cuando el jugador y el personaje se encuentren en la misma ubicación.
  • showOnScene. Valor booleano que indica si la imagen del personaje se debe mostrar cuando éste y el jugador se encuentren en la misma localización.
A la hora de interactuar con los personajes, éstos se comportan de una manera similar a los objetos, siendo posible realizar acciones que tengan a los mismos como objetos pasivos y habiendo efectos que modifican el valor de sus atributos.
Por supuesto, la gran diferencia entre los objetos y los personajes es que con éstos se puede hablar, pero ya entraremos en ese berenjenal en la próxima entrada.

Ale, a personajear.