DEV Community

Cover image for Decoding Wakfu's Action Effects with Javascript
Mark Kop
Mark Kop

Posted on

Decoding Wakfu's Action Effects with Javascript

Hi there!
Today I want to share how I managed to decode some kind of string logical template from an MMORPG spell's effects.

🤖 The Context

I'm developing a Discord Bot that retrieves data from a given Wakfu equipment. Fortunately, most of the required information is available thought some endpoints available in this forum post. One of them is the effect that an equipment can give according to its "action id".
The problem is this effect description comes with some variables inside the text that need to be decoded.

🐤 Example: easy

Royal Tofu Helmet

Equipment Royal Tofu Helmet from items.json

{
  "definition": {
    "item": {
      "id": 9481,
      "level": 18,
      // ...
    },
    "equipEffects": [
      {
        "effect": {
          "definition": {
            "id": 184439,
            "actionId": 1053,
            "areaShape": 32767,
            "areaSize": [],
            "params": [22, 0]
          }
        }
      },
      // ...
    ]
  },
  // ...
}

Enter fullscreen mode Exit fullscreen mode

And this is its action from actions.json:

{
  "definition": {
    "id": 1053,
    "effect": "Gain : Maîtrise Distance"
  },
  "description": {
    "fr": "[#1] Maîtrise Distance",
    "en": "[#1] Distance Mastery",
    "es": "[#1] dominio distancia",
    "pt": "[#1] de Domínio de distância"
  }
}
Enter fullscreen mode Exit fullscreen mode

The [#1] code tell us to use the first parameter from the equipment action's definition. However, parameters come in pairs. In this case, the first parameter is a fixed value plus a value that scales with level: params[0] + params[1] * level22 + 0*18. So [#1] = 22.
So the description would be 22 Distance Mastery
Pretty simple so far.

🎃 Example: medium

Gelano

Let's take a look on another example:
Equipment Gelano

"definition": {
    "id": 127211,
    "actionId": 1068,
    "areaShape": 32767,
    "areaSize": [],
    "params": [
      30,
      0,
      3,
      0
    ]
  }
Enter fullscreen mode Exit fullscreen mode
  {
    "definition": {
      "id": 1068,
      "effect": "Gain : Maîtrise Élémentaire dans un nombre variable d'éléments"
    },
    "description": {
      "fr": "{[~3]?[#1] Maîtrise [#3]:[#1] Maîtrise sur [#2] élément{[>2]?s:} aléatoire{[>2]?s:}}",
      "en": "{[~3]?[#1] Mastery [#3]:[#1] Mastery of [#2] random{[=2]?:} element{[=2]?:s}}",
      "es": "{[~3]?[#1] Dominio[#3]:[#1] Dominio de [#2] elemento{[>2]?s:} aleatorio{[>2]?s:}}",
      "pt": "{[~3]?[#1] Domínio[#3]:[#1] Domínio sobre [#2] elemento{[>2]?s:} aleatório{[>2]?s:}}"
    }
  }
Enter fullscreen mode Exit fullscreen mode

Now we not only have [#2] and [#3], but a [~3] and [>2] as well.

By giving it a look, we can identify some condition expressions in the format {<condition>?<valueIfTrue>:<else>}.

Clearly, the expression {[>2]?s:} is there to give plural to words when something is higher than two.

Using the same logic, the entire expression checks for a condition to print [#1] Mastery [#3] or [#1] Mastery of [#2] random{[=2]?:} element{[=2]?:s}.

To understand what these symbols mean, we can check out the discoveries that 0M1N0U5 kindly shared in the game's forums.

Operators +, -, ~ to check the number of parameters.
<, >, = with two behaviours:

If it comes with a parameter on the left [2=3], I use it as a constant compared to the parameter on the right using the operator that is naturally between both.

Every time I solve a calculated parameter, it is stacked so I can use it later.

If it comes without a number on the left, then I read the value from the top of the stack and use it as a value and in the same way as mentioned above.

We believe that [~3] checks if the number of arguments is at least three.

By checking equipment's params we can see that it has two arguments (four values), so it evaluates to its else value.
Cool, now we have this {[=2]?:s} which is likely to be a bug, since other languages uses {[>2]?s:}.

The key here is that the [>2] condition refers to the last evaluated parameter.

So in the expression {[>2]?s:} we're checking if [#2] is bigger than two (or equal if going with the English description).

Here's how the Spanish expression could be converted to some javascript code:

let stack = 0
const hasThreeOrMoreArguments = params.length >= 6 // [~3]
const firstParam = () => {  // [#1]
    const value = params[0] + params[1] * level
    stack = value
    return value
}
const secondParam = () => { // [#2]
    const value = params[2] + params[3] * level
    stack = value
    return value
}
const thirdParam = () => { // [#3]
    const value = params[4] + params[5] * level
    stack = value
    return value
}
const isLastStackValueGreatherThanTwo = () => stack > 2 // [>2]
const plural = () => isLastStackValueGreatherThanTwo() ? 's' : '' // [>2]?s:

// {[~3]?[#1] Dominio[#3]:[#1] Dominio de [#2] elemento{[>2]?s:} aleatorio{[>2]?s:}}
const description = `${hasThreeOrMoreArguments ? 
    `${firstParam()} Dominio${thirdParam()}`
    :
    `${firstParam()} Dominio de ${secondParam()} elemento${plural()}} aleatorio${plural()}`
}`
Enter fullscreen mode Exit fullscreen mode

The description for this equipment would be 30 Dominio de 3 elementos aleatorios
What's odd in here is that equipment with three or more arguments would have something like 30 Dominio1 as description. However, there's no single equipment that satisfies this condition.

So far, so good.

🐲 Example: boss

Gray Mage's Wand

Now we can check the boss example: "Gray Mage's Wand"

{
  "definition": {
    "item": {
      "id": 23189,
      "level": 109,
      // ...
    },
    "useEffects": [
      {
        "effect": {
          "definition": {
            "id": 212575,
            "actionId": 1084,
            "areaShape": 32767,
            "areaSize": 1,
            "params": [
              2.4,
              0.201
            ]
          }
        }
      },
      // ...
    ]
  },
  // ...
}

Enter fullscreen mode Exit fullscreen mode
{
  "definition": {
    "id": 1084,
    "effect": "Soin : Lumière"
  },
  "description": {
    "fr": "Soin [el6] : [#1]{[+3]?% des PV:}{[+3]?{[1=3]? max:{[2=3]? courants:{[3=3]? manquants:{[4=3]? max:{[5=3]? courants:{[6=3]? manquants:}}}}}}:}{[+3]?{[4<3]? du lanceur:{[7<3]? de la cible:}}:}{[-2]?{[0=2]? [ecnbi] [ecnbr]:}:}{[+2]?{[2=2]? [ecnbi]:}:}{[+2]?{[1=2]? [ecnbr]:}:}",
    "en": "[el6] Heal: [#1]{[+3]?% of HP:}{[+3]?{[1=3]? max:{[2=3]? current:{[3=3]? lost:{[4=3]? max:{[5=3]? current:{[6=3]? lost:}}}}}}:}{[+3]?{[4<3]? of the caster:{[7<3]? of the target:}}:}{[-2]?{[0=2]? [ecnbi] [ecnbr]:}:}{[+2]?{[2=2]? [ecnbi]:}:}{[+2]?{[1=2]? [ecnbr]:}:}",
    "es": "Cura [el6]: [#1]{[+3]?% de los PdV:}{[+3]?{[1=3]? máx.:{[2=3]? actuales:{[3=3]? faltantes:{[4=3]? máx.:{[5=3]? actuales:{[6=3]? faltantes:}}}}}}:}{[+3]?{[4<3]? del lanzador:{[7<3]? del objetivo:}}:}{[-2]?{[0=2]? [ecnbi] [ecnbr]:}:}{[+2]?{[2=2]? [ecnbi]:}:}{[+2]?{[1=2]? [ecnbr]:}:}",
    "pt": "Cura [el6]: [#1]{[+3]?% dos PV:}{[+3]?{[1=3]? máx.:{[2=3]? atuais:{[3=3]? perdidos:{[4=3]? máx.:{[5=3]? atuais:{[6=3]? perdidos:}}}}}}:}{[+3]?{[4<3]? do lançador:{[7<3]? do alvo:}}:}{[-2]?{[0=2]?[ecnbi] [ecnbr]:}:}{[+2]?{[2=2]? [ecnbi]:}:}{[+2]?{[1=2]? [ecnbr]:}:}"
  }
},
Enter fullscreen mode Exit fullscreen mode

This might look crazy, but thanks to 0M1N0U5 we have all the information we need to solve it.
It basically reads as:
"Heals a given amount. If there are more than three arguments, it heals a percentage of HP. If the third argument is equal to one, it's about max HP. If equals to two, it's about current HP. If three, lost HP" and so on. The [el6] tag means "Light Element" and [ecnbi]/[ecnbr] some icon that I'm not sure what is.
And as the previous example, there's no equipment with three or more arguments, so the description ends up as [el6] Heal: 24

💻 The Code

Now that we've reached this far, we should be able to code some generic way to evaluate these expressions.
The strategy I've followed was to swap all conditions structures to javascript conditions ternary expressions inside string literals.
So {[>2]?s:} becomes

`${ stack > 2 ? 's' : '' }`
Enter fullscreen mode Exit fullscreen mode

for example.
In a similar way, I pre-calculate parameters values and swap [#1] to

`${ stack = value }`
Enter fullscreen mode Exit fullscreen mode

so the returned value becomes the value and stack value is updated.

I think it's easier to just show the code:

You can notice that some action ids require different calculation for its paremeters, even a hardcoding for Makabrafire equipment itself.

This gist is a replica from the parseEffect.js file from araknomecha-scrapper, a project that gathers wakfu data to build and provide information to Corvo Astral, the discord bot I've mentioned in the beginning of this article.

Here's the test file so you can check the results of this parsing and maybe tweak it yourself.

📜 The Conclusion

By checking all descriptions from actions.json, we could actually create a custom parser for each of them, specially if we don't take in account the cases where no equipment whatsoever falls into a given condition as mentioned earlier.
However, understanding the logic and implementing a parser for these codifications was challenging enough to be worth the time.
This post content is preeety specific and might help just a few number of people, but it's a cool knowledge to share ;D

A big thanks to 0M1N0U5 for sharing what they've discovered on the forums!

Top comments (1)

Collapse
 
visama profile image
Víctor Sánchez (Visama)

Hi, I was wondering if there's another approach that doesn't involve the use of eval()
I'm trying to follow your guide on my own, but I get kind of lost about how to make it work.
eval() wasn't working for me cause when I build the project it gives me a warning stating eval() is not safe.