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...