Volcano Island
A new world type (New World > World): an island with a volcano, a crater lake, ash fields and lava-tube caves. Viggo sells sulfur.
A new world type (New World > World): an island with a volcano, a crater lake, ash fields and lava-tube caves. Viggo sells sulfur.
Mod id example.volcanoisland, side both.
A code mod: build it with dotnet build (see Making mods) and put the output folder in the game's Mods folder.
mod.json
{
"id": "example.volcanoisland",
"name": "Volcano Island",
"version": "1.0",
"author": "Seeds & Circuits",
"side": "both",
"description": "A new world type (New World > World): an island with a volcano, a crater lake, ash fields and lava-tube caves. Viggo sells sulfur."
}
VolcanoIsland.cs
using SeedsAndCircuits.Mods;
using SeedsAndCircuits.Terrain;
using Unity.Mathematics;
using UnityEngine;
namespace VolcanoIsland
{
/// <summary>
/// A world type made by a mod: New World ▸ World ▸ "Volcano Island". The game generates its island as usual, then the
/// mod raises a volcano in the middle with a crater lake, covers its slopes in ash (a biome of its own), digs more
/// caves and lets Viggo sell sulfur. Every machine runs the same code on the same seed, so host and players get the
/// same volcano (side "both": joining players download the mod).
/// </summary>
public sealed class VolcanoIsland : Mod
{
const string WorldTypeId = "example.volcano";
TerrainBiomeDefinition _ash;
TerrainMaterialDefinition _ashGround;
Texture2D _ashTexture;
public override void OnLoad()
{
AddWorldType(WorldTypeId, "Volcano Island");
// Undone last (registrations are undone in reverse), after the game let go of the biome and material.
WhenTurnedOff(() =>
{
Object.Destroy(_ash);
Object.Destroy(_ashGround);
Object.Destroy(_ashTexture);
});
// A terrain material of its own: dark volcanic ash, soft to dig, giving sand. Its texture is made here (a mod
// with a bundle would load a TerrainMaterialDefinition asset with real textures); any size works.
_ashTexture = AshTexture(256);
_ashGround = ScriptableObject.CreateInstance<TerrainMaterialDefinition>();
_ashGround.name = "Volcanic Ash";
_ashGround.displayName = "Volcanic Ash";
_ashGround.topAlbedo = _ashTexture;
_ashGround.debugColor = new Color(0.22f, 0.21f, 0.2f);
_ashGround.tilingMeters = 3f;
_ashGround.smoothness = 0.05f;
_ashGround.variation = 0.2f;
_ashGround.hardness = 0.6f;
_ashGround.dropItemId = "sand";
AddTerrainMaterial(_ashGround);
// A biome made in code (or a TerrainBiomeDefinition asset from a bundle): ash ground, no trees or grass.
_ash = ScriptableObject.CreateInstance<TerrainBiomeDefinition>();
_ash.name = "Ash Fields";
_ash.displayName = "Ash Fields";
_ash.color = new Color(0.35f, 0.33f, 0.32f);
_ash.grassDensity = 0f;
_ash.groundMaterial = _ashGround;
AddBiome(_ash);
// Content works the same way: everything added here is removed again when the mod is turned off.
AddTrade("viggo", "sulfur", price: 6, stock: 20);
}
/// <summary>Dark grey grit with lighter specks.</summary>
static Texture2D AshTexture(int size)
{
var texture = new Texture2D(size, size, TextureFormat.RGBA32, true) { name = "Volcanic Ash", wrapMode = TextureWrapMode.Repeat };
var pixels = new Color32[size * size];
var random = new System.Random(7);
for (int y = 0; y < size; y++)
for (int x = 0; x < size; x++)
{
// Tileable noise: the sum of waves that repeat across the texture.
float n = 0.5f + 0.25f * Mathf.Sin(2f * Mathf.PI * (3 * x + 5 * y) / size) * Mathf.Cos(2f * Mathf.PI * (4 * y - 2 * x) / size);
float grain = (float)random.NextDouble();
byte v = (byte)Mathf.Clamp(38 + n * 30 + grain * 22 + (grain > 0.97f ? 70 : 0), 0, 255);
pixels[y * size + x] = new Color32(v, (byte)(v * 0.96f), (byte)(v * 0.92f), 255);
}
texture.SetPixels32(pixels);
texture.Apply(true, false);
return texture;
}
static bool IsVolcano(TerrainWorldGeneration world) => world.type == WorldTypeId;
/// <summary>
/// The start island (generated worlds also have outer islands, raised one by one as they're made; their maps
/// start at <see cref="ProceduralWorldParams.Offset"/>). The volcano only goes on the start island.
/// </summary>
static bool IsStartIsland(ProceduralMap map) => math.all(map.Params.Offset == 0f);
/// <summary>Lower hills elsewhere, so the volcano stands out.</summary>
public override void OnConfigureWorld(TerrainWorldGeneration world, ref ProceduralWorldParams landscape)
{
if (!IsVolcano(world))
return;
landscape.MountainHeight *= 0.4f;
landscape.LandShare = math.max(landscape.LandShare, 0.55f);
}
/// <summary>The cone and its crater, straight into the height map (640 x 640 cells over the map and a sea margin).</summary>
public override void OnMapGenerated(TerrainWorldGeneration world, ProceduralMap map)
{
if (!IsVolcano(world) || !IsStartIsland(map))
return;
float2 center = Center(world);
float radius = world.size * 0.16f; // foot of the cone
float peak = math.min(130f, world.size * 0.06f);
float craterRadius = radius * 0.16f;
float craterFloor = peak - 22f;
int res = map.Res;
for (int z = 0; z < res; z++)
for (int x = 0; x < res; x++)
{
int i = x + z * res;
float2 p = map.Origin + new float2(x, z) * map.Cell;
float d = math.distance(p, center);
if (d >= radius)
continue;
// A concave cone (steep near the top) with a rim, the crater dug into it.
float t = 1f - d / radius;
float cone = peak * t * t * (3f - 2f * t) * (0.6f + 0.4f * t);
float h = math.max(map.Heights[i], cone + map.Heights[i] * (1f - t));
if (d < craterRadius)
h = math.lerp(craterFloor, h, math.smoothstep(craterRadius * 0.6f, craterRadius, d));
map.Heights[i] = h;
if (map.Kinds[i] == ProceduralMap.Sea && h > 1f)
map.Kinds[i] = ProceduralMap.Land;
}
// The crater lake: a lake the game fills with water (ProceduralMap.Lakes, cells marked as lake).
float water = craterFloor + 6f;
int cells = 0;
for (int z = 0; z < res; z++)
for (int x = 0; x < res; x++)
{
int i = x + z * res;
float2 p = map.Origin + new float2(x, z) * map.Cell;
if (math.distance(p, center) < craterRadius && map.Heights[i] < water)
{
map.Kinds[i] = ProceduralMap.LakeCell;
cells++;
}
}
map.Lakes.Add(new ProceduralLake
{
Center = center, Radius = craterRadius, WaterLevel = water, Depth = water - craterFloor, Cells = cells,
});
}
/// <summary>Ash on the volcano's slopes, ragged at the edge.</summary>
public override void OnBiomesGenerated(TerrainWorldGeneration world, TerrainBiomePainter biomes)
{
if (!IsVolcano(world) || !IsStartIsland(biomes.Map))
return;
float2 center = Center(world);
float radius = world.size * 0.16f * 0.75f;
for (int z = 0; z < biomes.Cells; z++)
for (int x = 0; x < biomes.Cells; x++)
{
float2 p = biomes.CellCenter(x, z);
float edge = radius * (1f + 0.15f * noise.snoise(p / 70f + world.seed % 1000));
if (math.distance(p, center) < edge)
biomes.SetCell(x, z, _ash);
}
}
/// <summary>More and bigger caves under a volcano (lava tubes).</summary>
public override void OnConfigureGround(TerrainWorldGeneration world, ref TestGeneratorParams ground)
{
if (IsVolcano(world))
ground.CaveDensity *= 1.5f;
}
/// <summary>The middle of the map (the island's middle).</summary>
static float2 Center(TerrainWorldGeneration world) => new float2(world.size * 0.5f, world.size * 0.5f);
}
}
VolcanoIsland.csproj
<Project Sdk="Microsoft.NET.Sdk">
<!-- Build settings and game references: ../Mod.props. Output: bin/Volcano Island/ -->
<PropertyGroup>
<AssemblyName>VolcanoIsland</AssemblyName>
<ModFolder>Volcano Island</ModFolder>
</PropertyGroup>
<Import Project="../Mod.props" />
</Project>