Making Ultimate ADOM moddable

The idea for Ultimate ADOM originally was born from a technical discussion – at Roguelike Celebration 2016 Josh Ge, mastermind behind the wonderful game Cogmind, pointed me towards so-called entity component systems (ECS). Then and still a pretty hyped kind of architecture for games.

I looked into them during my autumn vacation 2016, decided that I felt like testing some new map generation algorithms, started building a prototype using an ECS architecture for vast cavern maps (1000 x 1000 tiles anyone? that’s more than eight times the size of all maps in current ADOM combined!) and noticed that programming with ECS was fun.

Then I tested some more features and finally came to the conclusion that a roguelike game based on a pure ECS architecture being used for anything in the game (no matter if we are talking about monsters, the player, map tiles, items, skills or other special abilities) might be an evolutionary step in roguelike gaming because it introduces a level of flexibility that even Nethack can’t compete with. Maybe Dwarven Fortress. Probably Dwarven Fortress. Definitely Dwarven Fortress. But in different respects. And we work very hard on having a much simpler user interface.

So what’s all this ECS stuff about? The brief summary is that everything in the game is technically considered to be a so-called entity (basically just a uniquely identifiable something) and then behavior and state gets added to an entity by enhancing it with components. You can find more detailed information in my talks from Roguelike Celebration 2017 and Roguelike Celebration 2018:

For privacy reasons YouTube needs your permission to be loaded. For more details, please see our Privacy Policy.
I Accept

 

For privacy reasons YouTube needs your permission to be loaded. For more details, please see our Privacy Policy.
I Accept

So how does this actually feel in practice? Basically we have a content engine that is running in the background of Ultimate ADOM. This content engine is a layer above Unity – so Unity does all the graphics and sound stuff, the content engine (which we call AGE – for ADOM Game Engine) actually reads an internal configuration that describes all entities, entity templates and components defined for a game configuration, kicks the game off by instantiating specific entities and… *BOOM* You are playing an exciting roguelike game.

The external configuration in the case of AGE are defined based on JSON files. JSON has the advantage that it is easy to read and JSON files can be created with a plain text editor – all very nice when we are opening up Ultimate ADOM for modders (see below for more details on that).

The below example shows the definition of a battle axe:


{
  "Id": "BattleAxe",
  "BaseTemplates": ["Item", "OneHandedWeapon", "MeleeWeapon"],
  "Components":
  [
    {
      "Labeled":
      {
        "singular": "BattleAxeSingularName",
        "plural": "BattleAxePluralName"
      }
    },
    {
      "Slot":
      {
        "template": "Iron",
        "name": "Material"
      }
    },
    {"CausesDamage": {"amount": "1d6+2", "whenCombatMode": "MeleeCombat", "type": "Hacking"}},
    {"MinimumLevel": 1},
    {"Rarity": "Uncommon"},
    {"MonetaryValue": 15},
    {"Weight": 3500}
  ]
}

First you can see that the game makes entities extend other entities – a battle axe is an item, a one handed weapon and a melee weapon. The “Labeled” component is used for naming (and uses logical tag names in order to allow us to translate the in-game texts later on). The “CausesDamage” component states that the battle axe will cause 1d6+2 (3-8) points of hacking damage when used in melee combat. Battle axes can be generated on dungeon level 1 onwards (“MinimumLevel”). They are uncommon in occurrence (“Rarity”). It is worth 15 gold pieces (“MonetaryValue”), weighs 3.500 matric grams (“Weight”).

While this might not seem terribly exciting at first, you have to think about the possibilities: if you want to extend the game with a light saber (heaven forbid) you basically just will need to write a similar file, drop it into a predefined directory and *bam* there you go. Light saber in the game.

Monsters are slightly more complex but the pattern is the same:


{
  "Id": "Goblin",
  "BaseTemplates": ["Being", "Monster"],
  "Components":
  [
    {
      "Labeled":
      {
        "singular": "GoblinSingularName",
        "plural": "GoblinPluralName"
      }
    },
    {"Gendered": ["Male", "Female"]},
    {"Aligned": {"moral": "Lawful", "ethical": "Evil"}},
    {"HitPoints": "=1d8-1"},
    {"HitPointRecovery": {"oneEvery": 50}},
    {"Modifier": {"affects": "ToHit", "whenCombatMode": "MeleeCombat", "modification": "-1", "description": "GoblinMeleeMalusDescription"}},
    {"DefaultEquipment":
      [
        ["HandAxe", "ShortSword", "Whip", "Spear", "Spear", "HandAxe", "ShortSword"],
        ["", "", "LeatherCap", "MetalCap", "", ""],
        ["LeatherArmor", "LightFurs", "LightFurs", "LightFurs", "Rags", "Rags"],
        ["SmallShield", "", "", ""],
        ["LeatherBoots", "Sandals", "LeatherBoots", ""]
      ]
    },
    {
      "Slot":
      {
        "template": "GoblinRace",
        "name": "Race"
      }
    },
    {"MinimumLevel": 1},
    {"Rarity": "VeryCommon"},
    {"Speed": 100},
    {"CanLeaveCorpse": 15},
    {"Voice": {"sound": "ScreamsSound", "voice": "ScreamsVoice"}},
    {"Tracking": "Poor"},
    {
      "Description":
      {
        "verbosity": "Short",
        "description":
        [
          "GoblinShortDescription0",
          "GoblinShortDescription1"
        ]
      }
    },
    {
      "Description":
      {
        "verbosity": "Long",
        "description":
        [
          "GoblinLongDescription0",
          "GoblinLongDescription1"
        ]
      }
    }
  ]
}

I won’t go into the details about the goblin but most components should be self-explaining. Now what if you want to define something that we didn’t think about? Again easy – you can define new components (basically new vocabulary for defining things) by writing extensions in C#. Compile them to a DLL and again drop them in a predefined directory and you are good to go with your custom extension. So if you want to e.g. define an explosion ability for monsters you simply could write a C# class to add this as a component and then use the newly defined “Explosive” component in your JSON definitions. Easy.

In consequence we hope that Ultimate ADOM will work on tons of mods while we provide the tools to allow for exciting extensions… monsters, items and spells will be trivial, quests and maps a little more complex but still manageable. Take e.g. the map definition below which implements the demo parcours from my Ultimate ADOM Demo at Roguelike Celebration 2018:


{
  "Id": "COCDemoParcours",
  "Components":
  [
    {
      "PredefinedMap":
      {
        "map":
        [
          "*************************************************************",
          "*###########################################################*",
          "*###########M###SSSSSSS##oooooo#####.......=====.......xxxx#*",
          "*###....#......#SSSSSSS##oooooo#####.....======........xxxx#*",
          "*###>@..+......+SSSSSSS+.oooooo+.........=====.........xxxx#*",
          "*###....#......#SSSSSSS##oooooo#####.......======......xxxx#*",
          "*###########F###SSSSSSS##oooooo#####........=======....xxxx#*",
          "*######################################+####################*",
          "*######################################.####################*",
          "*######################################+####################*",
          "*####$$$$$$$........#.._..#####.................############*",
          "*####$$$_$$$........#..!..#####yyCyyyCyyyCyyyCyy############*",
          "*####$$$!$$$..0.....+.........+CyyyCyyyCyyyCyyyC############*",
          "*####$$$$$$$........#.....#####yyyyyyyyyyyyyyyyy############*",
          "*####$$$$$$$........#.....#####yyCyyyCyyyCyyyCyy############*",
          "*####$$$$$$$#####.#############CyyyCyyyCyyyCyyyC############*",
          "*#######.########.#############yyfyyyfyyyfyyyfyy############*",
          "*#######+########.#############yyyyyyyyyyyyyyyyy############*",
          "*#######..........#############CyyyCyyyCyyyCyyyC############*",
          "*##############################yyfyyyfyyyfyyyfyy############*",
          "*##############################yyyyyyyyyyyyyyyyy############*",
          "*##############################CyyyCyyyCyyyCyyyC############*",
          "*##############################yyyyyyyyyyyyyyyyy############*",
          "*###########################################################*",
          "*************************************************************"
        ],
        "tileMapping":
        {
          "@": ["Floor", "StartPosition"],
          ">": ["Floor", "StairDown"],
          "*": "ImpenetrableWall",
          "#": "SolidWall",
          ".": "Floor",
          "/": ["OpenDoor", "Floor"],
          "_": ["Floor", "Altar"], 
          "!": "HallowedGround",
          "+": ["ClosedDoor", "Floor"],
          "C": ["Floor", "CrystalOfPower"],
          "G": ["Floor", "Ghost"],
          "f": ["Floor", "CrystalOfFire"],
          "=": ["Floor", "WaterSurface"],
          "s": ["Floor", "Skeleton"],
          "0": "PentagramOfBrainExchange",
          "$": 
          [
            "Floor", 
            {
              "entity":
              {
                "SequentialEntities":
                [
                  "PotionOfMana", "ScrollOfPower", "Spellbook", "WandOfDigging", "NecklaceOfTheSilverTongue",
                  "RingOfDamage", "Spear", "Quarterstaff", "Mace", "Flail", "Warhammer", "Scimitar",
                  "ShortSword", "Dagger", "BattleAxe", "Rapier", "LongBow", "ShortBow", "HeavyCrossbow",
                  "NormalArrow", "NormalQuarrel", "Ruby", "GoldPiece", "TwoHandedSword", "LargeHammer",
                  "GreatAxe", "GlaiveLongSting", "SmallShield", "LargeShield", "ChainMail", "ScaleMail",
                  "LeatherApron", "Clothes", "MetalCap", "LeatherBoots", "Sandals", "IronRation",
                  "ThickGauntlets", "LeatherGirdle", "Si", "Torch", "ThievesPicks", "PickAxe", "RoundKey",
                  "PotionOfHealing"
                ]
              }
            }
          ],
          "x":
          [
            "Floor",
            {
              "entity":
              {
                "SequentialEntities":
                [
                  "Goblin", "Skeleton", "Ghost", "Necromancer", "GiantSpider", "Jackal", "GiantFrog",
                  "WillOwisp", "Owlbear"
                ]
              }
            }
          ],
          "y":
          [
            "Floor",
            {
              "entity":
              {
                "SequentialEntities":
                [
                  "Goblin", "Skeleton", "Ghost", "Necromancer", "GiantSpider", "Jackal", "GiantFrog",
                  "WillOwisp", "Owlbear"
                ]
              },
              "goal": "Wait"
            }
          ],
          "o":
          [
            "Floor",
            {
              "entity": "Orc",
              "goal": "AmbushPlayer"
            }
          ],
          "S":
          [
            "Floor",
            {
              "entity": "LargeSpider",
              "goal": "AmbushPlayer",
              "factions": "SpiderPit"
            }
          ],
          "M":
          [
            "Floor",
            {
              "entity": "Minotaur",
              "goal": "AmbushPlayer"
            }
          ],
          "F":
          [
            "Floor",
            {
              "entity": "GiantFrog",
              "goal": "AmbushPlayer"
            }
          ]
        }
      }
    }
  ]
}

Given some basic understanding of data structures and JSON in particular you should be able to guess the meaning of the definitions above within in a couple of minutes.

Adding new graphics and monsters with animations will be more challenging as you will need a firm grasp of Unity3D and Spine, the tool we use to animate monsters in the game. But compared to the lengths that modders go in other games this still should be a breeze.

Let us know what you – as a modder – would like to see as far as modding capabilities go. The initial release most likely won’t cover all possibilities but we will grow this ASAP. Which comes rather natural because all the game is based on the ECS definitions and there are no hard-coded special cases.

P.S.: I felt kind of knighted when Tarn Adams, Dwarven Fortress coding god and awesome guy all around, at the end of Roguelike Celebration 2018 during our final evening party stated “Your ECS architecture and the things you showed are really impressive”. Receiving such feedback from one of my heroes of mind-boggling game development really made the trip worth it on its own (and it was a fantastic trip for many other reasons). Below you find a quick pic from the final evening (Tarn is the third guy from the right, Josh Ge in the center, Brian Walker of Brogue fame to the left of Josh and me on the far left) to finish this blog post:

The final evening at Roguelike Celebration 2018

The final evening at Roguelike Celebration 2018

2018-10-25T08:13:00+00:00October 25th, 2018|Whispers from the Creator|5 Comments

About the Author:

The Creator and Maintainer of ADOM and Ultimate ADOM.

5 Comments

  1. Chris G. Williams 10/25/2018 at 3:01 PM - Reply

    I’m a little unclear on the SequentialEntities part. I get it’s an array of entity, but how does it map to the level you show. For example, x appears 20 times in the room to the top right, but there are 9 entities in the array. Does it just repeat in the order specified, or is each x a random substitution from the list? Same with the $, there are 40 on the map, but 45 in the list.

    • admin 10/26/2018 at 5:45 PM - Reply

      Actually the sequential entities are created in sequence (maps are generated from top left to bottom right so that actually determines the sequence). If there are less entities in the SequentialEntities part than symbols on the map, the sequence starts again from the beginning. If there are more in the list than on the map, the final ones won’t be generated (but this allowed nicely to build maps step-wise… define all you want to have and then fill the map piece by piece). Naturally this could lead to mistakes because some parts never might be generated – which already caused me to ponder if we should introduce either a strict mode (or a strict flag) for the SequentialEntities that causes AGE to complain if not all are generated at least once. Such things would be rather trivial to add and will be driven by demand.

  2. UltraBlueCat 11/01/2018 at 5:03 PM - Reply

    Seems promising, especially the map part. I can already see the possibilities it brings…

  3. zoomi 11/02/2018 at 3:20 AM - Reply

    Good job.
    I like your configuration struct very much.
    But, I have something not quite understand: what is the use of “slot”?
    I know it is a Entity Template too. but what is it different from “baseTemplate”?
    how you use “slot” in specific circumstances?

  4. lpe88 download 11/08/2018 at 9:22 AM - Reply

    Plan you receive includes that they offer is the start into the world
    of affiliate online marketing. Are not able to just develop a post and then forget about this tool.
    Potatoes are very healthy and cheap in the process. https://918kiss.host/76-lpe88

Leave A Comment

This website uses cookies and third party services. More: Privacy Policy. Settings Ok

Cookies

The Description

Third Party Embeds