User:Kernelmethod/Sandbox:ZoneBuilders
This page is about modding. See the modding overview for an abstract on modding. |
This article has information that is missing or not up to par.
Reason: This article is currently a work-in-progress. Please check back later. |
Reason: This article is currently a work-in-progress. Please check back later.
Warning: this is an advanced modding topic that requires familiarity with C# and several other areas of modding. We recommend reading Modding:Worlds first. |
Zone builders are bits of code that allow you to shape the generation of a zone. At the lowest level of detail they are used to place and remove objects from zones; more broadly, they can be used to shape the geometry and encounters that a player runs into in a room.
The game provides a limited suite of generic builders that you may find useful for your mod. In general, if you are making small alterations to a zone you do not need to deal with zone builders. On the other hand, if you're planning to create your own maps with custom procedural generation, you will want to know how to write them by hand.
Introduction
Preamble: example code on this page
This page presents several examples that you can refer to for creating your own zone builders. If you wish to follow along with these examples, you should create a small example mod that starts with the following code:
Worlds.xml
<?xml version="1.0" encoding="utf-8" ?>
<worlds>
<world Name="ZBWorld" ZoneFactory="ZBWorldZoneFactory" DisplayName="zone builder test world">
<cell Name="ExampleCell">
<zone Level="10" x="1" y="1" Name="test zone">
<!-- Leave empty for now -->
</zone>
</cell>
</world>
</worlds>
ZoneFactories.cs
namespace XRL.World.ZoneFactories {
public class ZBWorldZoneFactory : IZoneFactory {
public override bool CanBuildZone(ZoneRequest Request) => false;
public override Zone BuildZone(ZoneRequest Request) {
var zone = new Zone(80, 25);
zone.ZoneID = Request.ZoneID;
return zone;
}
public override void AddBlueprintsFor(ZoneRequest Request) {
var cb = Blueprint.CellBlueprintsByName["ExampleCell"];
Request.Blueprints.Add(cb.LevelBlueprint[1, 1, 10]);
}
public override void AfterBuildZone(Zone zone, ZoneManager zoneManager) {
ZoneManager.PaintWalls(zone);
ZoneManager.PaintWater(zone);
}
}
}
This will create a miniature world, ZBWorld
, that we will use for testing. To teleport into this world, use the wish goto:ZBWorld.40.12.1.1.10
.
Using zone builders
When the player enters a new zone, the game builds the new zone using the following procedure:
- First, it identifies the appropriate zone factory for the world that the player is in (JoppaWorld, ThinWorld, etc.).
- The game then runs
CanBuildZone
on the zone factory to determine the method that it should use to generate the zone[1].- When it returns true, the zone factory calls the factory's
BuildZone
method to create the zone. - Otherwise, the zone factory calls
GenerateZone
to instantiate the zone andAddBlueprintsFor
.
- When it returns true, the zone factory calls the factory's
- In most cases in JoppaWorld, the game takes the
AddBlueprintsFor
route. In this case, the game looks up the terrain object for the zone and searchesWorlds.xml
for the zone blueprint that it should apply to the zone[2]. - The game then iterates over the zone builders defined by the blueprint and applies them successively to the new zone.
Designing a custom zone requires you to select the builders that you want to use for the zone and add them to the zone. In the majority of cases, you will do this statically through Worlds.xml
. As an example, here's the specification of MoonStairCell
(used to generate generic Moon Stair zones):
<cell Name="MoonStairCell" Inherits="DefaultJoppaCell" ApplyTo="TerrainMoonStair">
<zone Level="5-9" x="0-2" y="0-2" Name="sky above the Moon Stair" IndefiniteArticle="the" AmbientBed="Sounds/Ambiences/amb_bed_moonstair">
<builder Class="Sky"></builder>
</zone>
<zone Level="10" x="0-2" y="0-2" Name="Moon Stair" IndefiniteArticle="the" AmbientBed="Sounds/Ambiences/amb_bed_moonstair">
<builder Class="MoonStair"></builder>
<builder Class="FactionEncounters" Population="GenericFactionPopulation"></builder>
<music Track="Reflections of Ptoh" />
<postbuilder Class="ZoneTemplate:MoonStair"></postbuilder>
</zone>
<zone Level="11-15" x="0-2" y="0-2" Name="subterranean stair">
<builder Class="MoonStair"></builder>
<builder Class="PossibleCryotube"></builder>
<builder Class="FactionEncounters" Population="GenericFactionPopulation"></builder>
<music Track="Reflections of Ptoh" />
<postbuilder Class="ZoneTemplate:MoonStairCaves"></postbuilder>
</zone>
</cell>
Cell specifications are broken up by X, Y, and Z-levels. Depending on where the player is, they will trigger different builders appropriate to their location.
The other place that zone builders are often added is dynamically during world generation. For example, this is how Oboroqoru, Ape God's lair, goatfolk yurts, and dynamic villages are placed by JoppaWorldBuilder
. It is possible, but less common, to dynamically apply zone builders in other parts of the game.
Generic, pre-existing zone builders
The game provides a limited suite of zone builders that are sufficiently generic that they can be easily applied to any zone.
Builder name | Description |
---|---|
AddBlueprintBuilder | Adds an object with a specified Object parameter as a blueprint. In general it is preferable to use zone templates and population tables to add (non-structural) objects to zones, so this is primarily useful when you need to dynamically add on a zone builder. For example: this builder is used to place the chest containing the Ruin of House Isner during world generation.
|
AddWidgetBuilder | Adds an object to the (0, 0) cell of a zone. Typically this is a Widget object (see ObjectBlueprints/Widgets.xml ) although in theory it can be anything. An AddWidgetBuilder builder is added to the zone whenever you use the <widget/> tag in Worlds.xml . For example,
<widget Blueprint="Grassy" />
adds a |
Connecter | |
FactionEncounters | Adds encounters with legendary creatures sampled from a provided table. |
MapBuilder | This builder loads a map from a .rpm file. A MapBuilder is implicitly to a zone by the <map/> tag in Worlds.xml. For example, the map for the Yd Freehold is added as follows:
<map FileName="YdFreehold.rpm" />
|
Music | Adds a music track to a zone. A Music zone builder is implicitly added by the <music/> tag in Worlds.xml. For example, the music for the salt dunes is added with
<music Track="MoghrayiRemembrance" />
|
SolidEarth | Fills the entire map with shale. This is useful when you wish to carve out the geometry of your zone (i.e., specify the empty space rather than specify the filled space). |
StairConnector | |
StairsDown | Adds a set of stairs down to the next Z-level. You can specify X- and Y- coordinates to influence the region in which the stairs are placed. |
StairsUp | Adds a set of stairs up to the previous Z-level. You can specify X- and Y- coordinates to influence the region in which the stairs are placed. |
TileBuilding |
In general, the game's existing zone builders tend to be highly monolithic, so any zone builders not listed above are not recommended for use in creating new types of zones. For new zones modders should favor a more modular architecture.
The ZoneBuilderSandbox interface
ZoneBuilderSandbox
is the primary interface for creating new zone builders. Any builders that you add to the game will inherit from this class.
Most zone builders only need to define one method: BuildZone
. This method accepts a zone instance, and returns a boolean that signals whether or not subsequent zone builders should also run[3]. As an example, here is the definition for the SolidEarth
zone builder:
namespace XRL.World.ZoneBuilders;
public class SolidEarth
{
public bool BuildZone(Zone Z)
{
for (int i = 0; i < Z.Width; i++)
{
for (int j = 0; j < Z.Height; j++)
{
Z.GetCell(i, j).Clear();
Z.GetCell(i, j).AddObject(GameObjectFactory.Factory.CreateObject("Shale"));
}
}
return true;
}
}
This builder
- loops over each cell in the zone (incidentally: nowadays there is a
Zone.GetCells
method to make this even easier); - clears all objects that are currently in the cell; and
- adds a shale object to the cell.
When designing new zone builders, you should (where possible) prefer to create modular zone builders that can be easily reused. As a general rule, earlier builders should define coarse-grained detail about a zone such as layout and general geometry. Builders that come later should be focused on adding fine-grained details (e.g. individual rooms and encounters) to the zone.
Helpful utilities
In principle, ZoneBuilderSandbox
is all that you need in order to build a zone. In practice, you will want to rely on some other utilities when generating zones. This section will cover some of the most important utilities available to you.
EnsureAllVoidsConnected
ZoneBuilderSandbox
includes an EnsureAllVoidsConnected
that will create paths between empty spaces in your zone. This is particularly useful when you are carving out your zone from another material.
For example: consider the following zone builder:
namespace XRL.World.ZoneBuilders {
public class CreateVoids : ZoneBuilderSandbox {
public bool NoisyPath = true;
public bool BuildZone(Zone Z) {
foreach (var cell in Z.GetCells()) {
if (
cell.CosmeticDistanceTo(20, 12) < 4 ||
cell.CosmeticDistanceTo(50, 12) < 4
)
cell.Clear();
}
EnsureAllVoidsConnected(Z, pathWithNoise: NoisyPath);
return true;
}
}
}
This will create two pockets of empty space -- one centered at coordinates (20, 12), another centered at coordinates (50, 12) -- and connects them with EnsureAllVoidsConnected
. By setting pathWithNoise
we can make a somewhat randomized path between these voids.
Starting from the code in the introduction, update your Worlds.xml
to include the following:
<!-- ... -->
<zone Level="10" x="1" y="1" Name="test zone">
<builder Class="SolidEarth" />
<builder Class="CreateVoids" />
<widget Blueprint="Grassy" />
</zone>
<!-- ... -->
Here is an example of a zone that gets generated with these builders:
Pathfinding FindPath
If you want to create a path directly between two cells, you can use the FindPath
interface (which is also used by creature AI to find the shortest path between two points).
Here is an example of the CreateVoids
zone builder from the EnsureAllVoidsConnected
example using pathfinding (this time with a straight path between the voids).
using XRL.World.AI.Pathfinding;
namespace XRL.World.ZoneBuilders {
public class CreateVoids : ZoneBuilderSandbox {
public bool BuildZone(Zone Z) {
foreach (var cell in Z.GetCells()) {
if (
cell.CosmeticDistanceTo(20, 12) < 4 ||
cell.CosmeticDistanceTo(50, 12) < 4
)
cell.Clear();
}
var path = new FindPath(Z.GetCell(20, 12), Z.GetCell(50, 12));
foreach (var step in path.Steps)
step.Clear();
return true;
}
}
}
PlacePopulationInRegion
PlacePopulationInRegion
samples some objects from a population table and places them in a specified set of cells. It is commonly used to limit the area for an encounter. A more limited version of this is PlacePopulationInRect
which works specifically for rectangles.
The following zone builder demonstrates use of PlacePopulationInRegion
to create a little circular farm in the center of the zone:
using Genkit;
using System;
using System.Collections.Generic;
namespace XRL.World.ZoneBuilders {
public class CreateCircularFarm : ZoneBuilderSandbox {
public bool BuildZone(Zone Z) {
var region = new List<Location2D>();
foreach (var cell in Z.GetCells()) {
var dist = cell.CosmeticDistanceTo(40, 12);
if (dist > 6)
continue;
cell.Clear();
if (dist == 6)
cell.AddObject("BrinestalkStakes");
else
region.Add(cell.Location);
}
PlacePopulationInRegion(Z, region, "PigFarm");
// Add a little door
Z.GetCell(30, 12).Clear();
Z.GetCell(30, 12).AddObject("Brinestalk Gate");
return true;
}
}
}
Create a PopulationTables.xml
so that you can define the PigFarm
table:
<?xml version="1.0" encoding="utf-8" ?>
<populations>
<population Name="PigFarm">
<group Name="Creatures" Style="pickeach">
<object Blueprint="PigFarmer" />
<object Blueprint="Herding Dog" />
<object Blueprint="Farm Pig" Number="3-6" />
</group>
</population>
</populations>
And now add the zone builder it to your Worlds.xml
with
<zone Level="10" x="1" y="1" Name="test zone">
<builder Class="CreateCircularFarm" />
<widget Blueprint="Dirty" />
</zone>
This should produce a tiny farm that looks like the following:
Other utilities
Finally, here are some other less commonly-used utilities that you may nonetheless find helpful for your use case:
Name | Class | Description |
---|---|---|
Clear | XRL.World.Cell | Remove all objects from a cell. |
ClearWalls | XRL.World.Cell | Remove all walls from a cell (preserving any other objects that may be present). |
RequireObject | XRL.World.Cell | Place an object with the specified blueprint in the cell, if one does not already exist. |
ClearRect | XRL.World.ZoneBuilders.ZoneBuilderSandbox | Clears out all of the objects from a rectangle in a given zone. |
PlaceHut | XRL.World.ZoneBuilders.ZoneBuilderSandbox |
Tools for procedural generation
As a roguelike, Caves of Qud uses procedural generation (procgen) for many parts of the game. For zone building in particular it is important to know about some of the algorithms that the game uses for procgen so that you can build coherent, non-static zones. This section covers some of the most important algorithms in the game's toolbox.
FastNoise
An important component of procedural generation is creating noise: a randomized signal with special properties, depending on the application that you're targeting. Some examples of noise that are commonly used in procedural generation are Perlin noise, simplex noise, and fractal noise.
Caves of Qud provides two interfaces for generating noise. The first is the FastNoise
class, which can be used to generically sample from many different noise distributions. FastNoise
is used heavily throughout the Qud codebase, including:
- by the
Strata
zone builder (used by generic underground zones) to define wall layout based on the wall material[4]. - by the Moon Stair zone builder to define the layout of crystals[5].
- by the
RiverBuilder
zone builder in the pathfinding algorithm used to generate rivers, by assigning random weights to cells[6]. - by the
CrystalGrassy
andCrystalDirty
widgets to paint floor tiles in the Moon Stair[7][8].
In general, usage of FastNoise
entails instantiating a FastNoise
instance, setting noise generation parameters on it, and then sampling using GetNoise(x,y,z)
. For example, here's how the CrystalGrassy
part samples simplex noise:
namespace XRL.World.Parts;
[Serializable]
public class CrystalGrassy : IPart
{
[NonSerialized]
private static FastNoise fastNoise = new FastNoise();
// ...
public static double sampleSimplexNoise(string type, int x, int y, int z, int amplitude, float frequencyMultiplier = 1f)
{
fastNoise.SetSeed(getSeed(type));
fastNoise.SetNoiseType(FastNoise.NoiseType.SimplexFractal);
fastNoise.SetFrequency(0.25f * frequencyMultiplier);
fastNoise.SetFractalType(FastNoise.FractalType.FBM);
fastNoise.SetFractalOctaves(3);
fastNoise.SetFractalLacunarity(0.7f);
fastNoise.SetFractalGain(1f);
return Math.Ceiling((double)fastNoise.GetNoise(x, y, z) * (double)amplitude);
}
// ...
}
Explanations of all of the different noise types and parameters are outside the scope of this tutorial. However, we strongly recommend trying out the noise exploration tool from the Auburn/FastNoiseLite repository, either by installing it and running it locally or by running it in the browser.
As a practical example, here's a zone builder that simply creates a wall distributed using sampled noise:
namespace XRL.World.ZoneBuilders {
public class RandomWalls : ZoneBuilderSandbox {
public bool BuildZone(Zone Z) {
var noise = new FastNoise();
noise.SetSeed(0);
noise.SetNoiseType(FastNoise.NoiseType.SimplexFractal);
noise.SetFrequency(0.05f);
noise.SetFractalType(FastNoise.FractalType.FBM);
noise.SetFractalOctaves(3);
noise.SetFractalLacunarity(0.7f);
noise.SetFractalGain(1f);
// Z.wX/Z.wY are the X/Y coords of the parasang; X and Y are the
// coordinates within the parasang.
int dX = (3 * Z.wX + Z.X) * 80;
int dY = (3 * Z.wY + Z.Y) * 25;
foreach (var C in Z.GetCells()) {
if (noise.GetNoise(C.X + dX, C.Y + dY, Z.Z) >= 0)
C.AddObject("Shale");
}
return true;
}
}
}
Try adding it to the Worlds.xml
provided to you in the introduction by adding the following:
<!-- ... -->
<zone Level="10" x="1" y="1" Name="test zone">
<builder Class="RandomWalls" />
<widget Blueprint="Dirty" />
</zone>
<!-- ... -->
Here's an example of some walls generated by this noise:
Compared this with the type of walls that we generate when we use cellular noise:
noise.SetSeed(0);
noise.SetNoiseType(FastNoise.NoiseType.Cellular);
noise.SetFrequency(0.05f);
noise.SetCellularDistanceFunction(FastNoise.CellularDistanceFunction.Natural);
noise.SetCellularReturnType(FastNoise.CellularReturnType.CellValue);
noise.SetCellularJitter(0.1f);
The walls generated by this kind of noise are blocky relative to the smoothness of simplex noise.
NoiseMap
The other utility class that is frequently used for noise generation is NoiseMap
(in XRL.World.ZoneBuilders.Utility
). This is also used extensively throughout the game, for example:
- it is used to distribute water in Lake Hinnom and in River[9][10].
- it is used to distribute clumps of trees in the outskirts of Bey Lah[11].
- it is used to make some random regions of walls holographic in the chuppah of Girsh Qas and Girsh Qon[12].
NoiseMap
provides a few less customizability options than FastNoise
, and doesn't support as many different types of noise. On the other hand, it allows modders to explicitly define regions of high or low noise.
Algorithm
Unlike FastNoise
, NoiseMap
only supports one algorithm for noise generation. At a high level, NoiseMap
creates a two-dimensional grid of random values and then iteratively convolves it with a custom filter, blurring the noise in a manner similar to how box linear filtering and Gaussian blurring work. The algorithm creates a few locations of high noise (randomly, and through noise map nodes that are passed in to the algorithm), which are then smoothed out through iterated convolutions.
More specifically, the algorithm used by NoiseMap
works as follows:
- Create a 2D
Noise
array, calledNoise
, and fill it with uniformly random values viaStat.Random(0, BaseNoise)
. - Divide the zone into
SectorsWide
xSectorsHigh
non-overlapping rectangles ("sectors"), then shuffle the list of sectors. - Iterate over each sector and place
Stat.Random(SeedsPerSector)
"seeds" in random (not necessarily exclusive) locations in the sector. Sample noise asStat.Random(MinSeedDepth, MaxSeedDepth)
at each seed location.- These seed locations serve as areas of high noise.
- Iterate over the list of
NoiseMapNode
s in theExtraNodes
list. At each node, increment the seed count and set the noise toMaxSeedDepth
(or the node'sdepth
, if specified).- These node locations also serve as locations of high noise.
- Now iterate over the zone
FilterPasses
times, convolving the noise grid with the 3 x 3 filter{{1,3,1},{3,6,3},{1,3,1}}
during each pass.
Example
As an example, let's use NoiseMap
to create a zone with four pre-defined pockets of empty space, one in each corner of the zone. We'll also configure our NoiseMap to create some extra seeds so that our zone has some additional random pockets of space.
using System.Collections.Generic;
using XRL.World.ZoneBuilders.Utility;
namespace XRL.World.ZoneBuilders {
public class TestNoiseMap : ZoneBuilderSandbox {
public bool BuildZone(Zone Z) {
var nodes = new List<NoiseMapNode>();
nodes.Add(new NoiseMapNode(10, 5));
nodes.Add(new NoiseMapNode(10, 20));
nodes.Add(new NoiseMapNode(70, 5));
nodes.Add(new NoiseMapNode(70, 20));
int MaxDepth = 10; // This parameter doesn't actually do anything
int SectorsWide = 3;
int SectorsHigh = 3;
int SeedsPerSector = 2;
int MinSeedDepth = 80;
int MaxSeedDepth = 80;
int BaseNoise = 4;
int FilterPasses = 5;
int BorderWidth = -3;
int CutoffDepth = 1;
var noiseMap = new NoiseMap(
Z.Width, Z.Height, MaxDepth, SectorsWide, SectorsHigh,
SeedsPerSector, MinSeedDepth, MaxSeedDepth, BaseNoise,
FilterPasses, BorderWidth, CutoffDepth, nodes
);
foreach (var cell in Z.GetCells()) {
if (noiseMap.Noise[cell.X, cell.Y] <= 2)
continue;
cell.ClearWalls();
}
EnsureAllVoidsConnected(Z, pathWithNoise: true);
return true;
}
}
}
Use the following builders for your Worlds.xml
:
<builder Class="SolidEarth" />
<builder Class="TestNoiseMap" />
<widget Blueprint="Dirty" />
Here's an example of a zone generated using this builder. We can see that the zone contains pockets of empty space at the (10, 5), (10, 20), (70, 5), and (70, 20) cells in the corners, but it's also got some additional pockets via the seeds added by NoiseMap
.
[File:Noisemap_example.webp]
Wave function collapse
Caves of Qud makes extensive use of the wave function collapse (WFC) for certain kinds of procedural generation. In particular, it is commonly used to procedurally generate different kinds of buildings, such as huts, crypts, lairs, and so on. Examples of where WFC is used include:
- generating several areas in the Tomb of the Eaters, including the crypts[13][14] and the gardens[15].
- generating various structures in historic sites[16], which in turn gets called by other zonebuilders such as
Strata
. - creating initial structures when generating villages[17].
From a modding perspective, WFC can be thought of as an black box that takes in a colormap as input and produces a new colormap that is locally similar to the original as output. In the typical usage of WFC, the inputs it receives are formatted as .png files stored in the wavetemplates/
directory of the game's data files. If you wish to use WFC in a mod, you can either use one of the predefined templates or a template from the wavetemplates/
directory in your own mod. Each of these templates is a colormap containing pixels in a few different colors. For example, here is the compound.png
wave template from the game's data files:
There are several different interfaces you can use to run WFC; the one we will show in this article is the WaveCollapseFastModel
class. You can instantiate this class by passing in the wave template you want to use, the height and width of the area you want to generate, and a handful of other parameters.
using Genkit;
using XRL.Rules;
namespace XRL.World.ZoneBuilders {
public class TestWFC : ZoneBuilderSandbox {
public string Template = "spiral";
public bool BuildZone(Zone Z) {
var wfcModel = new WaveCollapseFastModel(Template, 3, 40, 20, true, false, 8, 0);
wfcModel.Run(Stat.Random(int.MinValue, int.MaxValue), 0);
var colorMap = new ColorOutputMap(wfcModel);
for (int i = 0; i < colorMap.width; i++) {
for (int j = 0; j < colorMap.height; j++) {
string wallType = null;
var color = colorMap.getPixel(i, j);
if (WaveCollapseTools.equals(color, ColorOutputMap.BLACK))
wallType = "Basalt";
else if (WaveCollapseTools.equals(color, ColorOutputMap.RED))
wallType = "Shale";
else if (WaveCollapseTools.equals(color, ColorOutputMap.MAGENTA))
wallType = "ChavvahTrunk";
else if (WaveCollapseTools.equals(color, ColorOutputMap.GREEN))
wallType = "Serpentinite";
else if (WaveCollapseTools.equals(color, ColorOutputMap.BLUE))
wallType = "Coral Rag";
else if (WaveCollapseTools.equals(color, ColorOutputMap.YELLOW))
wallType = "Gypsum";
if (!wallType.IsNullOrEmpty())
Z.GetCell(i, j).AddObject(wallType);
}
}
return true;
}
}
}
Add this to your Worlds.xml
with
<zone Level="10" x="1" y="1" Name="test zone">
<builder Class="TestWFC" Template="huts11" />
<widget Blueprint="Dirty" />
</zone>
Try playing around with the Template
parameter and feed a few different WFC templates into the builder. You should see various structurally similar zones appear in the zones that you generate. For example, here are two zones generated with the pillars1
and huts11
templates, respectively.
Zone templates
The final step of zone generation is adding encounters -- creatures, objects, or events that the player might encounter in the zone. For this you will generally want to use zone templates.
A zone template is typically applied as a postbuilder on a zone:
<zone Level="10" x="0-2" y="0-2" Name="Palladium Reef" IndefiniteArticle="the" AmbientBed="Sounds/Ambiences/amb_bed_palladiumReef">
<builder Class="Reef" />
<builder Class="FactionEncounters" Population="GenericFactionPopulation" />
<music Track="Substrate" />
<postbuilder Class="ZoneTemplate:PalladiumReef" />
</zone>
The line <postbuilder Class="ZoneTemplate:PalladiumReef" />
tells the game to apply the PalladiumReef
zone template defined in ZoneTemplates.xml
after all the other builders have run.
ZoneTemplates.xml
ZoneTemplates.xml
has a relatively complex structure that allows modders to layer several criteria on top of one another before placing objects from a population.
We start with a simple example, the Mushroomy
zone template:
<zonetemplate Name="Mushroomy" RegionSize="100">
<global>
<population Table="MushroomyGlobal"></population>
</global>
</zonetemplate>
All this zone template says is that objects should be sampled from the MushroomyGlobal
population table and spawned around the zone, without much care for the area in which they spawn.
For many situations, a zone template with this structure is sufficient. However, you will often want to have more control over where and how objects are placed. For example: if you're trying to set up a zone simulating a battle between two warring factions, you probably want creatures from one faction to appear in the left half of the zone, and creatures from the other faction appear in the right half.
We will start with an overview of the structure of ZoneTemplates.xml
, and in particular the tags that it supports.
XML tag | Meaning |
---|---|
<zonetemplate>
|
Defines a new zone template. Once defined, the zone template can be invoked by name from Worlds.xml using postbuilders, e.g.
<postbuilder Class="ZoneTemplate:Rivers" />
|
<global>
|
Defines a set of instructions that should apply over the entire zone. |
<eachregion>
|
Defines a set of instructions that should apply over each contiguous region of the zone. |
<single>
|
|
<group>
|
|
<cellfilterout>
|
|
<exit>
|
If the conditions to reach those node are met, then we stop executing the zone template on the region that we are currently processing. |
InfluenceMapBlocker
Examples
We will work through an example to demonstrate some of the capabilities of zone templates. Start by updating your Worlds.xml
to the following:
<zone Level="10" x="1" y="1" Name="test zone">
<widget Blueprint="Dirty" />
<postbuilder Class="ZoneTemplate:ZTTest" />
</zone>
Now create a ZoneTemplates.xml
with the content below:
<?xml version="1.0" encoding="utf-8" ?>
<zonetemplates>
<zonetemplate Name="ZTTest" RegionSize="200">
<global>
<population Table="EmptyField" />
</global>
<eachregion>
<group Chance="30" Criteria="RegionHasSemanticTag:Inside">
<population Table="SnapjawParty0" />
</group>
<group Chance="30" Criteria="RegionHasSemanticTag:Rect1">
<population Table="BaboonParty" />
</group>
<group Chance="30" Criteria="RegionHasSemanticTag:Rect2">
<population Table="IssachariParty" />
</group>
</eachregion>
</zonetemplate>
</zonetemplates>
Observe that so far we haven't actually added any structure to our test world's zones, so the only part of the zone template above that will actually apply is the part between the <global>...</global>
tags. Indeed, if we teleport into our test world, we should find it bare modulo some content from the EmptyField
population table:
Now we will add a zone builder which layers on some additional structure. Create the following zone builder ZTExample
:
using Genkit;
namespace XRL.World.ZoneBuilders {
public class ZTExample : ZoneBuilderSandbox {
public bool BuildZone(Zone Z) {
var rect1 = new Rect2D(20, 7, 30, 17);
var rect2 = new Rect2D(50, 7, 60, 17);
PlaceHut(Z, rect1);
PlaceHut(Z, rect2);
for (int x = rect1.x1; x <= rect1.x2; x++) {
for (int y = rect1.y1; y < rect1.y2; y++) {
Z.GetCell(x, y).AddSemanticTag("Inside");
Z.GetCell(x, y).AddSemanticTag("Rect1");
}
}
for (int x = rect2.x1; x <= rect2.x2; x++) {
for (int y = rect2.y1; y < rect2.y2; y++) {
Z.GetCell(x, y).AddSemanticTag("Inside");
Z.GetCell(x, y).AddSemanticTag("Rect2");
}
}
return true;
}
}
}
This builder creates two rectangular regions, and adds semantic tags to all of the cells inside of those regions. The net result is that the first region is set up such that the two zonetemplate groups with criteria RegionHasSemanticTag:Inside
and RegionHasSemanticTag:Rect1
apply to the first region, while the zonetemplate groups with criteria RegionHasSemanticTag:Inside
and RegionHasSemanticTag:Rect2
apply to the second region.
Add the new builder to your Worlds.xml
with <builder Class="ZTExample" />
, and teleport into your world again. As you move across the different zones, you should observe that:
- snapjaws will occasionally spawn in one or both of the rectangular boxes;
- baboons will occasionally spawn in the left box;
- Issachari will occasionally spawn in the right box; and
- some dogthorn trees, arconauts, and other creatures will spawn throughout the entire zone (including, occasionally, within the boxes).
The semantic tags that we added to our cells impacted what creatures and objects spawned throughout our region. Here are a few screenshots of some zones generated by this zone builder / zone template combination:
You should try painting one of the rectangles with InfluenceMapBlocker
s, for example
/* In first loop */
Z.GetCell(x, y).AddSemanticTag("Inside");
Z.GetCell(x, y).AddSemanticTag("Rect1");
Z.GetCell(x, y).AddObject("InfluenceMapBlocker");
You should observe that after adding these blockers, objects stop spawning in the rectangle.
References
- ↑
XRL.World.ZoneManager
, methodGenerateFactoryZone
- ↑
XRL.World.ZoneFactories.JoppaWorldZoneFactory
, methodAddBlueprintsFor
- ↑
XRL.World.ZoneManager
, methodApplyBuilderToZone
- ↑
XRL.World.ZoneBuilders.Strata
- ↑
XRL.World.ZoneBuilders.MoonStair
, methodBuildZone
- ↑
XRL.World.ZoneBuilders.RiverBuilder
, methodBuildZone
- ↑
XRL.World.Parts.CrystalGrassy
, methodPaintCell
- ↑
XRL.World.Parts.CrystalDirty
, methodPaintCell
- ↑
XRL.World.ZoneBuilders.OverlandAlgaeLake
, methodBuildZone
- ↑
XRL.World.ZoneBuilders.OverlandWater
, methodBuildZone
- ↑
XRL.World.ZoneBuilders.BeyLahOutskirts
, methodBuildZone
- ↑
XRL.World.ZoneBuilders.QasQonLair
, methodHoloZone
- ↑
XRL.World.ZoneBuilders.CatacombsMapTemplate
, methodGenerate
- ↑
XRL.World.ZoneBuilders.CryptOfLandlords
, methodBuildZone
- ↑
XRL.World.ZoneBuilders.CryptOfWarriors
, methodBuildZone
- ↑
XRL.World.ZoneBuilders.SultanDungeon
, methodBuildZoneFromArgs
- ↑
XRL.World.ZoneBuilders.VillageBase
, methodaddInitialStructures