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.

Saturday, May 25, 2019

Las Pifias Vandálicas de Le Pédant II. ¿Tautología?¿Sí?¿No?¿Mnñeeeeeeh?

Tenemos hoy una pifia buena buena que ya llevaba tiempo queriendo poner por aquí, pero como soy así me había olvidado de dónde la había visto.

E.Turco = E.Islamico
=OTAN=?

Sí, se ha comido la tilde que debería ir en "islámico" pero, por supuesto, la ortografía es el menor problema de nuestro vándalo de hoy.

En primer lugar, un consejo: Si vas a plasmar en una pintada una reivindicación conspiranoide como ésta, no pongas un signo de interrogación. No hay nada que mine más una teoría de la conspiración que la falta de seguridad por parte del que la propone.

Para seguir, y como ya dijimos en la pifia anterior, no hay que pasarse a la hora de ser conciso. Porque, de verdad, qué coño quiso decir exactamente el que pintó esto.

Y para terminar ¿La OTAN? ¿En serio? Estamos en 1980 o qué.

Tuesday, May 07, 2019

Las Joyas Vandálicas de Le Pédant XXX. Illuminati confirmed?

Desde Ponferrada me han mandado esta modesta joya llena de piadoso fervor religioso.

"Dios mola"

Bueno, o está llena de piadoso fervor religioso o es una confirmación más de la ubicua presencia, subrepticias tácticas y omnímodo poder de los Illuminati, ahí cada uno ya que vea lo que quiera.

Sea como sea, sólo decir una cosa: ya, ya sé que molo mogollón, pero gracias de todos modos.

Friday, April 12, 2019

Mientras tanto, en Plutón...

Después del atracón de disparadores vamos con una cosa más ligera, el contenido del archivo adventure.json.

Sí, empezamos por el medio

El contenido de adventure.json define características generales del juego que no tienen cabida en otros archivos y que serán almacenados en un objeto tipo Adventure.java contenido en la clase AdventureApplication.java, que hereda de la clase Application.java y será instanciada al lanzar la aplicación. El contenido del archivo adventure.json de La Aventura Original tiene este aspecto:

{
   "initialLocation":1,
   "introText":"¡La Gran Caverna! Origen de leyendas sobre fabulosas riquezas e inmensos peligros. ¿Mito o realidad?\n\nSiguiendo el borroso mapa que te dio un extraño (por cierto muy rico) a quien salvaste de las garras de unos trolls, has llegado a este apacible valle.\n\nPresientes que estás muy cerca, pero de repente el viejo mapa se deshace en tus manos mientras una cálida brisa te susurra: 'sólo los valientes que dominen la magia merecerán mis tesoros'...",
   "introButtonText":"COMENZAR LA AVENTURA",
   "introImage":"intro_image.png",
   "adventureName":"La Aventura Original",
   "inventoryLimit":7
}

La función que cumple cada elemento es bastante evidente:

  • initialLocation. El identificador de la localización inicial del jugador.
  • introText. El texto que se mostrará en la pantalla de introducción del juego si se quiere usar la pantalla por defecto.
  • introButtonText. El texto que se mostrará en el botón que permite iniciar la partida en la pantalla de introducción del juego. Del mismo modo que en el caso de introText, el diseñador puede optar por usar una pantalla de introducción personalizada.
  • introImage. Lo han adivinado, el nombre del archivo de imagen que se mostrará en la pantalla de introducción.
  • adventureName. El nombre del juego.
  • inventoryLimit. El límite de objetos que puede llevar el personaje. Lo más común es que los objetos tengan un peso de 1, lo que limitaría el número de objetos que el personaje puede cargar a 7, pero es posible hacer que un objeto ocupe más -o menos- espacio en el inventario tal y como hemos visto al hablar de los objetos.

En un futuro será en este archivo donde se definan los diferentes módulos que pueden formar la aventura, por ahora todos los objetos, localizaciones y otros elementos del juego se cargan en un sólo bloque.

Ale, a aventurear.

Saturday, April 06, 2019

Mientras tanto, en Plutón...

No sé si se acuerdan, pero cuando en su momento hablamos de las localizaciones nos quedaron pendientes un par de atributos que tenían relación con los disparadores y cuando hablamos de los propios disparadores había por ahí algún atributo cuyo funcionamiento no estaba implementado. A que no adivinan de qué vamos a hablar hoy.

Más diversión con localizaciones

Los atributos de la clase GameLocation que habían quedado pendientes son onEnterTriggers y onExitTriggers. Estos atributos contienen un array de objetos GameTrigger que se comprobarán cada vez que el jugador entre o salga de una localización en lugar de como respuesta a una acción realizada con un objeto. Aparte de esta diferencia en la condición de activación, el resto del funcionamiento de estos disparadores es idéntico al que explicamos en la entrada anterior cuando hablamos de las acciones.

El otro atributo es locationId, un atributo de la clase GameTrigger que especifica el identificador de la localización en la que el disparador puede ser activado. La implementación de esta funcionalidad fueron, literalmente, dos líneas de código, pero es la típica cosa que vas dejando atrás mientras te enfangas con cosas más críticas.

Y eso es todo por hoy. Ale, a cazar tesoros.

Thursday, April 04, 2019

Life in the Static

De tanto hablar de motores y jotasones corremos el riesgo de olvidar para qué estamos aquí: para perder el tiempo. Así pues, aquí les dejo un jueguete para perder 15 minutos de su vida.


De nada.

Wednesday, March 27, 2019

Mientras tanto, en Plutón...

En esta entrada vamos a examinar los cuatro atributos de la clase GameItem que nos quedan pendientes. Se trata de una cosa un tanto compleja, intentaré explicarme lo mejor posible.

Antes de proceder con la explicación vamos a echarle un vistazo al contenido del archivo game_actions.xml de la versión 0. En este archivo se definían las acciones que podía llevar a cabo el jugador, o más exactamente las acciones que tenían un resultado específico más allá de las acciones con resultados predeterminados, como "mirar", "examinar" o las órdenes de movimiento.

Usaremos como ejemplo la acción "USAR PILA LINTERNA", definida en el XML como sigue:

<actions>
   <action>
      <actionId>17</actionId>
      <actionObject_1>-1</actionObject_1>
      <actionSubstituteObject_1>0</actionSubstituteObject_1>
      <actionObject_2>-2</actionObject_2>
      <actionSubstituteObject_2>-3</actionSubstituteObject_2>
      <locationEffect>0</locationEffect>
      <locationSubstitute>0</locationSubstitute>
      <actionMessage>Insertas la pila en el interior de la linterna.</actionMessage>
   </action>

...
</actions>

El identificador de la acción se especificaba en actionId, en este caso el valor 17 corresponde a la acción "usar".

El objeto activo -en este caso la pila, cuyo identificador es 1 tal y como ya vimos en el archivo game_items.xml- se definía en actionObject.
Si este valor era negativo uno de los resultados de la acción es la eliminación del objeto y su sustitución por el objeto indicado en actionSubstituteObject_1. En nuestro ejemplo actionSubstituteObject_1 tiene valor 0, por lo que el objeto activo no se sustituía por ningún otro objeto.

El objeto pasivo, en este caso la linterna, se definía en actionObject_2. Al igual que en el caso de los objetos activos, si el valor de actionObject_2 era negativo debía eliminarse el objeto y sustituirse por el indicado en actionSubstituteObject_2.

Los valores de locationEffect y locationSubstitute funcionaban de manera similar, indicando una localización que se vería afectada por la acción y el identificador de la localización que debía tomar su lugar.

Finalmente, actionMessage definía el mensaje que se debía mostrar al llevar a cabo la acción.

Bastante apañado, pero evidentemente muy limitado. Cada acción podía tener efecto sólo sobre dos objetos que debían además ser parte de la acción y este efecto se limitaba a la creación o eliminación de objetos. Supongo que no extrañará a nadie que las acciones fuesen de lo primero a lo que hinqué el diente cuando empecé la versión actual y que apenas haya sobrevivido nada.

Trigger warning

Al abordar la nueva versión decidí pasar la definición de las acciones a los objetos que toman parte en ellas. Estas acciones se definen en una serie de atributos dentro de la clase GameItem:
  • onPickTriggers 
  • onDropTriggers 
  • onExamineTriggers 
  • onActionTriggers
Estos cuatro arrays contienen los disparadores que se activarán al realizar determinadas acciones con el objeto en el que se definen: al añadir el objeto al inventario, al dejar el objeto en una localización desde el inventario, al examinar el objeto y al realizar una acción que no se incluya en los tres casos anteriores.

Como vimos en el primer artículo sobre los objetos la definición de la acción "METER PILA LINTERNA" tiene este aspecto:

"onActionTriggers":[
      {
        "id":1,
        "triggerType":1,
        "triggerSubType":0,
        "beenTriggered":"false",
        "actionId":16,
        "locationId":null,
        "itemId":1,
        "characterId":null,
        "enabled":"true",
        "effects":[
          {
            "id":1,
            "type":1001,
            "passiveGameItems":null,
            "passiveGameLocations":null,
            "passiveGameNPCs":null,
            "passiveGameTriggers":null,
            "passiveGameFlags":null,
            "newIntegerValue":null,
            "newBooleanValue":null,
            "newStringValue":"Metes la pila en la linterna",
            "message":null
          },{
            "id":2,
            "type":136,
            "passiveGameItems":[2],
            "passiveGameLocations":null,
            "passiveGameNPCs":null,
            "passiveGameTriggers":null,
            "passiveGameFlags":null,
            "newIntegerValue":null,
            "newBooleanValue":true,
            "newStringValue":null,
            "message":null
          },{
            "id":3,
            "type":401,
            "passiveGameItems":null,
            "passiveGameLocations":null,
            "passiveGameNPCs":null,
            "passiveGameTriggers":null,
            "passiveGameFlags":[1],
            "newIntegerValue":null,
            "newBooleanValue":true,
            "newStringValue":null,
            "message":null
          }
        ],
        "conditionalFlags":[
          {
            "id":1,
            "name":"Pila en linterna",
            "type":1,
            "booleanValue":"false",
            "integerValue":null
          }
        ],
        "activeItemConditions":null,
        "passiveItemConditions":null
      }


Los diferentes atributos que podemos ver son:
  • id. Identificador numérico del disparador.
  • triggerType. Tipo de disparador. Define el tipo de acción que se debe llevar a cabo para activar el disparador.
  • triggerSubtype. Tipo secundario. Actualmente un disparador puede tener un tipo secundario "normal", "one-shot" o "execute and break", estando reservado el segundo tipo para aquellos disparadores que sólo puedan ser activados una vez y el tercero para los disparadores que deban impedir que el resto de disparadores definidos en el objeto se comprueben si el mismo ha sido activado.
  • beenTriggered. Valor booleano que indica si un disparador ha sido activado. Sólo es relevante al usarse con el tipo secundario "one-shot".
  • actionId. El identificador numérico de la acción que activa el disparador.
  • locationId. No implementado. En un futuro servirá para limitar las localizaciones en las que se puede llevar a cabo una acción.
  • itemId. Identificador numérico del objeto pasivo de la acción, si existe.
  • characterId. Identificador numérico del personaje que actúa como objeto pasivo de la acción, si existe.
  • enabled. Valor booleano que indica si un disparador puede o no activarse.
  • conditionalFlags. Array de objetos GameFlag. Los flags definidos en flags.json deben tener el mismo valor que el definido en el disparador para que éste pueda activarse. Pueden ver una de las entradas anteriores para saber más sobre los flags.
  • activeItemConditions. Un array de objetos tipo GameFlag que definen requisitos específicos que debe cumplir el objeto activo.
  • passiveItemConditions. Un array de objetos tipo GameFlag que definen requisitos específicos que debe cumplir el objeto pasivo.
  • effects. Un array que contiene los efectos que se ejecutarán si el disparador es activado con éxito.
Al recibir la orden para ejecutar la instrucción "METER PILA LINTERNA" el intérprete divide la misma en partes, detectando la acción "METER", el objeto activo "PILA" y el objeto pasivo "LINTERNA" y procede con los siguientes pasos:
  1. El motor comprueba que el objeto activo tiene definidos disparadores en el atributo onActionTriggers y busca aquellos cuyo triggerType corresponda al tipo de acción que se quiere realizar -en este caso, el tipo genérico "acción"-, que el atributo id del objeto pasivo se corresponde con el atributo itemId del disparador y que el atributo actionId del disparador se corresponde con el identificador de la acción interpretada -en este caso "meter", cuyo identificador es 16-.
  2. El atributo enabled tiene valor true.
  3. Si el disparador es de tipo "one-shot" el atributo beenTriggered tiene valor false.
  4. El valor de los flags definidos en el disparador corresponde al valor de los mismos en flags.json.
  5. Los flags definidos en activeItemConditions se corresponden con los atributos del objeto activo.
  6.  Los flags definidos en pasiveItemConditions se corresponden con los atributos del objeto pasivo.
Si se cumplen estas condiciones, el motor pasará a ejecutar los efectos definidos en el disparador. Es en estos efectos, definidos en el array effects, donde reside toda la potencia. Los efectos se definen mediante la clase GameEffect, cuyos atributos son:
  • id. Identificador numérico del efecto.
  • type. Tipo de efecto, este valor determinará el resultado del mismo.
  • passiveGameItems. Objetos que se verán afectados por el efecto.
  • passiveGameLocations. Localizaciones que se verán afectadas por el efecto.
  • passiveGameNPCs. Personajes que se verán afectados por el efecto.
  • passiveGameTriggers. Disparadores que se verán afectados por el efecto.
  • passiveGameFlags. Flags que se verán afectados por el efecto.
  • newIntegerValue. Nuevo valor entero que tomará el atributo modificado por el efecto.
  • newBooleanValue. Nuevo valor booleano que tomará el atributo modificado por el efecto.
  • newStringValue. Nueva cadena de texto que se asignará al atributo modificado por el efecto.
  • message. Mensaje que se mostrará al ejecutar el efecto.
En nuestro ejemplo, los efectos a ejecutar tienen los identificadores 1001 -mostrar el mensaje definido en newStringValue por la salida de texto-, 136 -asigna al atributo implicit del objeto pasivo el valor indicado en newBooleanValue- y 401, que asigna a los flags especificados en el array passiveGameFlags el valor de newBooleanValue. Así, al meter la pila en la linterna se mostrará al jugador el mensaje "Metes la pila en la linterna", la pila pasará a ser un objeto implícito y el flag con identificador 1 que indica si la pila está en la linterna o no pasará a tener valor true.

¿Se han enterado de algo? Si es así, enhorabuena, porque yo mismo he tenido que darle un buen repaso al código para acordarme de todo.

Monday, March 25, 2019

Mientras tanto, en Plutón...

Seguimos con el análisis de la definición de objetos que empezamos en la anterior entrada. En esta ocasión veremos los atributos más "normales" que no estaban en la versión primitiva del motor, dejando la definición de acciones y disparadores para la próxima entrada.

Esta es CASI mi forma final

La clase gameItem tiene un pié puesto en la versión final, con la excepción de un par de atributos relacionados con el ruido que puede hacer un objeto y una posible revisión de cómo funciona el empujar, tirar y mover objetos, claro.
  • inventariable. Un valor booleano que indica si el objeto puede ser añadido al inventario del jugador.
  • movable, pushable, pullable. Valores booleanos que indican si un objeto si el jugador puede tirar de un objeto, empujarlo o moverlo. Actualmente movable no se utiliza y pushable y pullable se usan como condición necesaria para ejecutar las acciones "empujar" y "tirar".
  • weight. Un valor entero que indica el peso del objeto. A efectos prácticos el peso indica el espacio que el objeto ocupa en el inventario, de modo que el jugador no podrá llevar en su inventario objetos cuyo peso supere el límite de inventario definido en adventure.json.
  • lightSource. Un valor entero que indica el nivel de luz emitido por el objeto. Pueden echarle un vistazo al funcionamiento de las fuentes de luz en esta entrada de la serie.
  • noisy, noiseDescription. Actualmente pendientes de implementación. La idea es que el atributo noisy indique si el objeto hace ruido y noiseDescription se añada a la descripción de la localización si hay un objeto ruidoso en la localización o en una localización adyacente.
  • visibility. Un valor entero que indica el valor mínimo que deben tener las fuentes de luz presentes en la localización para que el personaje pueda ver el objeto. El jugador no podrá interactuar con aquellos objetos que no pueda ver.
  • image, largeImage. Los nombres de los archivos de imagen que se usarán en las listas de objetos de inventario y de objetos en la localización actual en el caso de image o en la vista de detalle del objeto a pantalla completa, si se usa, en el caso de largeImage.
  • implicit. El funcionamiento del atributo implicit se explicó en la primera entrada de esta serie. Los objetos implícitos serán aquellos que forman parte del inventario del personaje, pero no ocupan espacio, no se muestran en el panel de objetos de inventario -ya hablaremos sobre este panel cuando lleguemos a la interfaz de usuario- y el personaje no podrá interactuar con ellos de la misma manera que con los objetos no implícitos.
Como ven, nada demasiado complicado. Por ahora.

Monday, March 18, 2019

Mientras tanto, en Plutón...

Si las localizaciones son la columna vertebral de una aventura los objetos son, no sé, el fluido linfático. Sí, por qué no.

Objetos orientados a objetos

Como ya hicimos con la definición de las localizaciones, vamos a comparar cómo se definían los objetos en la versión primitiva y actual del motor.

En tiempos remotos los objetos se definían en el archivo game_items.xml, que tenía este aspecto:

<items>
   <item>
      <itemId>1</itemId>
      <itemName>Pila</itemName>
      <itemDescription>Una pila eléctrica, un místico artefacto capaz de dar energía a ciertos objetos.</itemDescription>
      <itemLocationText> En el suelo puedes ver una pila.</itemLocationText>
      <itemType>0</itemType>
      <itemLocation>10</itemLocation>
      <characterDialog> </characterDialog>
   </item>
   <item>
      <itemId>2</itemId>
      <itemName>Linterna</itemName>
      <itemDescription>Una linterna, es capaz de dar luz en lugares oscuros... si dispusiese de una fuente de energía.</itemDescription>
      <itemLocationText> En el suelo puedes ver una linterna.</itemLocationText>
      <itemType>0</itemType>
      <itemLocation>0</itemLocation>
      <characterDialog> </characterDialog>
   </item>
...
</items>

Una definición bastante sencilla con un identificador único, un nombre, dos descripciones -una, itemDescription, para cuando el objeto se examina y otra, itemLocationText, para añadir al texto descriptivo de la localización en la que está el objeto-, un identificador de tipo de objeto -en este caso 0, correspondiente a los objetos que pueden ser añadidos al inventario-, el identificador de la localización en la que se encuentra el objeto y el diálogo que se mostrará cuando el jugador hable con el objeto porque de aquella los personajes se consideraban objetos y sólo tenían una línea de diálogo posible. Sí eran tiempos oscuros.

Ahora la definición de los objetos va en el archivo game_items.json y es considerablemente más densa. Los mismos objetos "pila" y "linterna" tienen ahora este aspecto:

[
  {
    "id":1,
    "names":["Linterna"],
    "description":"Una linterna, es capaz de dar luz en lugares oscuros... si dispusiese de una fuente de energía.",
    "onLocationDescription":"En el suelo puedes ver una linterna.",
    "itemListDescription":"Una linterna, es capaz de dar luz en lugares oscuros... si dispusiese de una fuente de energía.",
    "location":0,
    "inventariable":"true",
    "movable":null,
    "pushable":null,
    "pullable":null,
    "weight":1,
    "lightSource":0,
    "noisy":null,
    "noiseDescription":null,
    "onPickTriggers":null,
    "onDropTriggers":null,
    "onActionTriggers":[
        {
            "id":1,
            "triggerType":1,
            "triggerSubType":0,
            "beenTriggered":"false",
            "actionId":18,
            "locationId":null,
            "itemId":1,
            "characterId":null,
            "enabled":"true",
            "effects":[
              {
                "id":1,
                "type":1001,
                "passiveGameItems":null,
                "passiveGameLocations":null,
                "passiveGameNPCs":null,
                "passiveGameTriggers":null,
                "passiveGameFlags":null,
                "newIntegerValue":null,
                "newBooleanValue":null,
                "newStringValue":"Enciendes la linterna. Un haz de luz emerge de uno de sus extremos.",
                "message":null
              },
              {
                "id":2,
                "type":131,
                "passiveGameItems":[1],
                "passiveGameLocations":null,
                "passiveGameNPCs":null,
                "passiveGameTriggers":null,
                "passiveGameFlags":null,
                "newIntegerValue":2,
                "newBooleanValue":null,
                "newStringValue":null,
                "message":null
              }
            ],
            "conditionalFlags":[
              {
                "id":1,
                "name":"Pila en linterna",
                "type":1,
                "booleanValue":"true",
                "integerValue":null
              }
            ],
            "activeItemConditions":null,
            "passiveItemConditions":null
        },
        {
            "id":2,
            "triggerType":1,
            "triggerSubType":0,
            "beenTriggered":"false",
            "actionId":19,
            "locationId":null,
            "itemId":1,
            "characterId":null,
            "enabled":"true",
            "effects":[
              {
                "id":1,
                "type":1001,
                "passiveGameItems":null,
                "passiveGameLocations":null,
                "passiveGameNPCs":null,
                "passiveGameTriggers":null,
                "passiveGameFlags":null,
                "newIntegerValue":null,
                "newBooleanValue":null,
                "newStringValue":"Apagas la linterna.",
                "message":null
              },
              {
                "id":2,
                "type":131,
                "passiveGameItems":[1],
                "passiveGameLocations":null,
                "passiveGameNPCs":null,
                "passiveGameTriggers":null,
                "passiveGameFlags":null,
                "newIntegerValue":0,
                "newBooleanValue":null,
                "newStringValue":null,
                "message":null
              }
            ],
            "conditionalFlags":[
              {
                "id":1,
                "name":"Pila en linterna",
                "type":1,
                "booleanValue":"true",
                "integerValue":null
              }
            ],
            "activeItemConditions":null,
            "passiveItemConditions":null
        }
    ],
    "onExamineTriggers":null,
    "visibility":1,
    "image":"item.png",
    "largeImage":null,
    "implicit":false
  },
  {
    "id":2,
    "names":["Pila eléctrica","pila electrica","pila"],
    "description":"Una pila eléctrica.",
    "onLocationDescription":"Puedes ver una pila.",
    "itemListDescription":"Una pila eléctrica.",
    "location":10,
    "inventariable":"true",
    "movable":null,
    "pushable":null,
    "pullable":null,
    "weight":1,
    "lightSource":0,
    "noisy":null,
    "noiseDescription":null,
    "onPickTriggers":null,
    "onDropTriggers":null,
    "onActionTriggers":[
      {
        "id":1,
        "triggerType":1,
        "triggerSubType":0,
        "beenTriggered":"false",
        "actionId":16,
        "locationId":null,
        "itemId":1,
        "characterId":null,
        "enabled":"true",
        "effects":[
          {
            "id":1,
            "type":1001,
            "passiveGameItems":null,
            "passiveGameLocations":null,
            "passiveGameNPCs":null,
            "passiveGameTriggers":null,
            "passiveGameFlags":null,
            "newIntegerValue":null,
            "newBooleanValue":null,
            "newStringValue":"Metes la pila en la linterna",
            "message":null
          },{
            "id":2,
            "type":136,
            "passiveGameItems":[2],
            "passiveGameLocations":null,
            "passiveGameNPCs":null,
            "passiveGameTriggers":null,
            "passiveGameFlags":null,
            "newIntegerValue":null,
            "newBooleanValue":true,
            "newStringValue":null,
            "message":null
          },{
            "id":3,
            "type":401,
            "passiveGameItems":null,
            "passiveGameLocations":null,
            "passiveGameNPCs":null,
            "passiveGameTriggers":null,
            "passiveGameFlags":[1],
            "newIntegerValue":null,
            "newBooleanValue":true,
            "newStringValue":null,
            "message":null
          }
        ],
        "conditionalFlags":[
          {
            "id":1,
            "name":"Pila en linterna",
            "type":1,
            "booleanValue":"false",
            "integerValue":null
          }
        ],
        "activeItemConditions":null,
        "passiveItemConditions":null
      },
      {
        "id":2,
        "triggerType":1,
        "triggerSubType":0,
        "beenTriggered":"false",
        "actionId":17,
        "locationId":null,
        "itemId":1,
        "characterId":null,
        "enabled":"true",
        "effects":[
          {
            "id":1,
            "type":1001,
            "passiveGameItems":null,
            "passiveGameLocations":null,
            "passiveGameNPCs":null,
            "passiveGameTriggers":null,
            "passiveGameFlags":null,
            "newIntegerValue":null,
            "newBooleanValue":null,
            "newStringValue":"Sacas la pila de la linterna",
            "message":null
          },{/
            "id":2,
            "type":136,
            "passiveGameItems":[2],
            "passiveGameLocations":null,
            "passiveGameNPCs":null,
            "passiveGameTriggers":null,
            "passiveGameFlags":null,
            "newIntegerValue":null,
            "newBooleanValue":false,
            "newStringValue":null,
            "message":null
          },{
            "id":3,
            "type":401,
            "passiveGameItems":null,
            "passiveGameLocations":null,
            "passiveGameNPCs":null,
            "passiveGameTriggers":null,
            "passiveGameFlags":[1],
            "newIntegerValue":null,
            "newBooleanValue":false,
            "newStringValue":null,
            "message":null
          },{
            "id":4,
            "type":131,
            "passiveGameItems":[1],
            "passiveGameLocations":null,
            "passiveGameNPCs":null,
            "passiveGameTriggers":null,
            "passiveGameFlags":null,
            "newIntegerValue":0,
            "newBooleanValue":false,
            "newStringValue":null,
            "message":null
          }
        ],
        "conditionalFlags":[
          {
            "id":1,
            "name":"Pila en linterna",
            "type":1,
            "booleanValue":"true",
            "integerValue":null
          }
        ],
        "activeItemConditions":null,
        "passiveItemConditions":null
      }
    ],
    "onExamineTriggers":null,
    "visibility":1,
    "image":"item.png",
    "largeImage":null,
    "implicit":false
  },

...
]

Sí, explicar todo esto me va a llevar unas cuantas entradas. Pero no nos precipitemos y demos un repaso a los atributos que tienen en común.

  • id. Correspondería al itemId, el identificador único del objeto.
  • names. En lugar de un nombre único ahora se define una lista de nombres, de modo que el jugador pueda escribir uno de esos nombres cuando introduzca una instrucción para ser ejecutada. En el ejemplo actual el jugador podría escribir la instrucción "coger pila", "coger pila eléctrica" o "coger pila electrica" obteniendo el mismo resultado al ejecutarla.
  • description, itemListDescription. Se corresponden con itemDescription. El primer valor se muestra al examinar el objeto, el segundo valor es opcional y muestra una descripción más detallada si se está usando la opción de mostrar los objetos examinados desde el panel de objetos a pantalla completa.
  • onLocationDescription. Se corresponde a itemLocationText.
  • location. Correspondiente a itemLocation, la localización donde se encuentra el objeto. Si el valor de este atributo es 0 el objeto se encontrará en el inventario del jugador. Si es menor que 0, normalmente -1, el objeto no estará en ninguna localización ni en el inventario del jugador, no existiendo a efectos prácticos.

Como se puede ver ni el tipo de objeto ni el diálogo existen, debido a que la "inventariabilidad" -sí, me lo acabo de inventar- del objeto ahora se maneja de manera diferente y a que los personajes tienen ahora su propia definición y tratamiento.

Pero todo eso es otra historia...