WorldForge-ArtImport

From Stormhalter
Jump to: navigation, search

WorldForge Art import workflow and tutorial.

When WorldForge starts for the first time, it creates a subfolder in the current directory called ".storage". It caches some files there so that it can be used if the server is offline, but it also creates a configuration file to enable GitHub integration so artists can develop new terrain sprites and test them before creating pull requests.

Before beginning this process, ensure you have cloned the Stormhalter repo using a git tool like GitHub Desktop. If you need help with this process, consult the Stormhalter Discord.

To configure WorldForge, open the [.storage\CustomArt.cfg] file and modify the first line to point to the Content folder of your local repo. If you wish to disable GitHub integration, simply delete this file and it will be regenerated with the default path.

Once configured, WorldForge will now prefer the \Data\Terrain-External.xml and \WorldForge\Components.xml files when loading, over the ones in the .Bin files shipped with Stormhalter. You can make edits to these files and they will be reflected in the editor.

At this time, the only way to reload these files is to restart WorldForge entirely. A "hot-reload" feature should be forthcoming.

Overview

The process for importing new art has three major steps:

  1. Create sprite sheets or individual sprites as .png images
  2. Create Terrain definitions in Data/Terrain-External.xml
  3. Create Component definitions in WorldForge\Components.xml

Creating Sprite Sheets

Actually creating the sprites is not in scope for this document. The relevant details, though, are that the sprites need to be either 55x55px (flat floor tiles) or 100x100px(any tile with depth that overlaps another tile). Sprites need to be saved as PNG files, and the engine supports the alpha channel for transparency. Multiple sprites can be in a single image, the Terrain XML definitions will define the region of the sprite sheet to be used.

Updating Terrain-External.xml

Review the comment at the top of Terrain-External.xml. Be aware of the details for cost, order, and underpile, as these have effects on gameplay.

The texture element is a relative path from the Content folder to the sprite sheet, but does not contain the file extension: [Korazail\Terrain\floors] would cause WorldForge to look for a sprite sheet at [Content\Korazail\Terrain\floors.png].

The source element is in the format (left, top, width, height). If you juxtapose the left and top coordinates, as long as your sprite sheet is not square, WorldForge will alert at startup that the source is out of bounds.

The average element is an approximate color of the tile if you squint at it hard from a long way away. It is used for making minimaps, though none are exposed to players at this time.

You will need to come up with unique Terrain IDs for your tiles. You can see the existing used IDs by looking at the Static dropdown in the WorldForge Components picker. At the time of this writing, Tenser is using 622-784 and is a regular contributor. There are thousands of available IDs, so give other contributors some padding when choosing your ranges. If you happen to collide with an existing Terrain ID, Worldforge will alert at startup.

Your terrain XML elements can go at the end of the file, or in order with existing Terrain IDs.

Updating Components.xml

Unlike the Terrain-External.xml file, the Components XML has structure and a more complicated syntax. Under the Data XML Node, there are several Category nodes. These are used for grouping in WorldForge and the names are free-form. A component could exist in multiple categories, though they would have to be manually kept in sync in the case of changes. We want to avoid category bloat, but if your components don't fit into an existing category, you can create a new one. New categories should be based on either a theme or a function, not a contributor; a world builder will be looking for either a specific functional component, like a door, or a palette to build with, like 'Nomad tents.'

Individual components are a way to put a name to a Terrain element, along with associated details. For example, a Door is really three distinct terrains: open door, closed door, destroyed door. the DoorComponent links the three Terrains into one object a world builder can place in the game.

The following components types, and their syntax, exist as of March 2022 and are unlikely to change:

       Floor-ish Components:

        <FloorComponent name="Light">
            <ground>1</ground> <!-- Terrain ID -->
            <movementCost>1</movementCost>
        </FloorComponent>

        <IceComponent name="Ice"> <!-- Players can slip when moving on IceComponents -->
            <ground>20</ground> <!-- Terrain ID -->
            <movementCost>1</movementCost>
        </IceComponent>

        <WaterComponent name="Water">
            <ground>22</ground> <!-- Terrain ID -->
            <depth>3</depth> <!-- Entities can drown in depth 3 water -->
            <movementCost>3</movementCost>
        </WaterComponent>

        <PoisonedWaterComponent name="Poisoned Water"> <!-- Poisoned water also gets a potency value in the editor -->
            <ground>180</ground> <!-- Terrain ID -->
            <movementCost>3</movementCost>
        </PoisonedWaterComponent>

        Things in the way:

        <CounterComponent name="Wood (H)">
            <counter>86</counter> <!-- Terrain ID -->
        </CounterComponent>

        <AltarComponent name="Temple Altar">
            <altar>113</altar> <!-- Terrain ID -->
        </AltarComponent>

        <ObstructionComponent name="Sandstone Pillar">
            <obstruction>313</obstruction> <!-- Terrain ID -->
            <blockVision>true</blockVision> <!-- Can the player see through the tile, or does it act like a wall -->
        </ObstructionComponent>

        Walls, Ruins, Doors:

        <WallComponent name="Town (V)">
            <wall>28</wall> <!-- Terrain ID -->
            <destroyed>41</destroyed> <!-- Terrain ID -->
            <ruins>45</ruins> <!-- Terrain ID -->
            <indestructible>false</indestructible>
        </WallComponent>

        <RuinsComponent name="Dungeon"> <!-- have a cost of 3 by default and enable hiding next to them -->
            <ruins>44</ruins> <!-- Terrain ID -->
        </RuinsComponent>

        <DoorComponent name="Town (V)">
            <openId>77</openId> <!-- Terrain ID -->
            <closedId>65</closedId> <!-- Terrain ID -->
            <secretId>28</secretId> <!-- Terrain ID -->
            <destroyedId>83</destroyedId> <!-- Terrain ID -->
            <isSecret>false</isSecret>
            <isOpen>false</isOpen>
        </DoorComponent>

        Special function components:

        <LockersComponent name="Lockers (V)">
            <static>120</static> <!-- Terrain ID -->
        </LockersComponent>

        <TrashComponent name="Trash Barrel"> <!-- destroys any items dropped onto the ground on it's tile -->
            <static>88</static> <!-- Terrain ID -->
        </TrashComponent>

        <TreeComponent name="Large Tree"> <!-- Trees have hardcoded growth pairings for now. Best to use ruins with a cost of 1 to emulate a tree -->
            <tree>98</tree> <!-- Terrain ID -->
            <canGrow>true</canGrow>
        </TreeComponent>

        Magical components:

        <Web>
            <static>131</static> <!-- Terrain ID -->
            <allowDispel>true</allowDispel>
        </Web>

        <Fire>
            <allowDispel>true</allowDispel>
        </Fire>

        <Darkness>
            <allowDispel>true</allowDispel>
        </Darkness>

        <Whirlwind>
            <allowDispel>true</allowDispel>
        </Whirlwind>

        "Teleporters":

        <SkyComponent name="Air">
            <destinationX>0</destinationX>
            <destinationY>0</destinationY>
            <destinationRegion>0</destinationRegion>
            <teleporterId>9</teleporterId> <!-- Terrain ID -->
        </SkyComponent>

        <EgressComponent name="Kesmai">
            <egress>316</egress> <!-- Terrain ID -->
            <destinationSegment>1</destinationSegment>
        </EgressComponent>

        <RopeComponent name="Up (North)">
            <destinationX>0</destinationX>
            <destinationY>0</destinationY>
            <destinationRegion>0</destinationRegion>
            <teleporterId>238</teleporterId> <!-- Terrain ID -->
            <isSecret>false</isSecret>
        </RopeComponent>

        <ShaftComponent name="Pit"> <!-- going "down" into a shaft will be a fall. Climbing down instead acts like a rope -->
          <destinationX>0</destinationX>
          <destinationY>0</destinationY>
          <destinationRegion>0</destinationRegion>
          <teleporterId>181</teleporterId> <!-- Terrain ID -->
        </Shaftcomponent>

        <StaircaseComponent name="Up (NS)">
            <destinationX>0</destinationX>
            <destinationY>0</destinationY>
            <destinationRegion>0</destinationRegion>
            <teleporterId>127</teleporterId> <!-- Terrain ID -->
            <descends>false</descends>
        </StaircaseComponent>

        And last but not least:

        <StaticComponent name="Fountain Pool">
            <static>209</static> <!-- Terrain ID -->
        </StaticComponent>

        All components can take an optional hueing element:
            <color r="0" g="255" b="0" a="255" />
        This sets the hue by default, but is not required.

Detailed Example

@Tenser#2262 on Discord has graciously provided us with a sample sprite sheet as an example for this process.

This will result in this animated campfire:
inline

The sprites are contained in this sprite sheet. This image uses the 100x100 size, with frames progressing along the x(left\right)-axis.
inline

To generate our Terrain-External XML, we need to create a Terrain with a sprite consisting of multiple frames. We also need a unique ID and to decide on the cost and order.

For Cost, this is a normal floor tile. Despite being a fire, it won't hurt entities who step on it, as tiles are considered to be several feet wide. Plenty of room to walk around the fire. Lets leave this at a cost of 1, though 2 could be acceptable if we consider navigating around the hazard to be slower than walking through a normal floor.

For Order, we want this tile to be rendered over floors, but under any doors that happen to open onto a campfire tile. This puts us at an Order of 0. When working with Order 0 sprites, we have to consider underpile, which decides whether the treasure sprite is rendered before or after our new sprite. In this case, treasure should probably be rendered on TOP of the campfire, so it is obvious. This defies physics somewhat, but makes gameplay sense. For this terrain, we will want an Underpile of true. Perhaps we could split this fire into two sets of sprites, and have a smoke sprite with an order of 6, so rendered over the pile and crits, but contained all in a single terrain. The possibilities!

The following XML snippet defines our Terrain:

   <terrain id="2000" cost="1">
      <!-- animated campfire from Tenser -->
      <sprite order="0" underpile=true>
        <texture>Korazail\Demo\fire-sheet</texture>
        <source>(0,0,500,100)</source> <!-- Note that the source here is the WHOLE thing. The frames are called out below. If you had only a single sprite, you would use something more like (0,0,100,100) for a 100x100px sprite. -->
        <frames step="100" keys="0-4"> <!--100ms between frames-->
          <frame>(0,0,100,100)</frame>
          <frame>(100,0,100,100)</frame>
          <frame>(200,0,100,100)</frame>
          <frame>(300,0,100,100)</frame>
          <frame>(400,0,100,100)</frame>
        </frames>
      </sprite>
      <average>(252,120,0,255)</average>
    </terrain>

Now that we have our Terrain, lets add it to the bottom of the current Data\Terrain-External.xml and load up WorldForge.

inline
Uh-oh. We had an issue in our XML. WorldForge exposes XML parsing errors as a popup during start. Depending on where an error occurs, either the whole custom art Terrain-External.xml is skipped or the specific Terrain is skipped. In the latter case, any components referencing that terrain will still load, but the art will be missing. Let me fix my typo by putting quotes around the true for the underpile attribute...

inline
WorldForge starts with a different error. I forgot to place the sprite sheet in the folder. Let me go fix that...
inline

WorldForge now starts without complaint - though it does alert us that it made a change to a special file. This file is used when Stormhalter is compiled. It pre-processes all the art assets into the various .BIN files that are distributed to the client. We need to ensure that our new art is sent to clients, or they won't see it. If we decide to rename the sprite sheet file, there is a similar message that reminds us to remove the other filename from this file.
inline

We can see our new campfire! WorldForge does not currently animate sprites, so we only see the first frame.

Next, let's define it as a Component. This is a simple one. This component doesn't do anything, cannot be interacted with and is thus a StaticComponent. Let's put it in the Decorations category, right at the top, before the Boxing Ring:

   ...
    <category name="Decorations">
      <StaticComponent name="Animated Campfire">
        <static>2000</static>
      </StaticComponent>
      <StaticComponent name="Boxing Ring">
    ...

If we had made any typos in the Components.xml, those would have been shown to us at load time as well, but we didn't, so WorldForge loads without issue and we can see our new campfire is a Decoration.
inline

Let's take a look at our GitHub Desktop changes tab to see what we did.
inline

A new file, and a few new lines to the Terrain-External and Components xml files and a modification to that [Content.Stormhalter-External.mgcb] to include our new art in the .BIN files. We're good to commit this and then submit a Pull Request. Since multiple artists might be working at the same time, and there are only three files that define the contributed art, there is potential for merge conflicts, though they should be straightforward to resolve.