Last Update: 2023-07-06
New Biome Project
This tutorial guides you through creating a complete mod that adds a new biome to Don't Starve Together. We'll create a Crystal Forest biome with unique terrain, vegetation, resources, and creatures.
Project Overview
By the end of this tutorial, you'll have created:
- A custom biome with unique ground textures
- Special vegetation and resources that spawn in the biome
- Custom creatures that inhabit the biome
- Integration with the existing world generation system
Prerequisites
- Intermediate understanding of Lua programming
- Familiarity with Don't Starve Together modding
- Basic knowledge of world generation
- Understanding of prefabs and components
Project Structure
CrystalForestMod/
├── modinfo.lua
├── modmain.lua
├── scripts/
│ ├── map/
│ │ ├── rooms/
│ │ │ └── crystal_forest.lua
│ │ └── tasks/
│ │ └── crystal_forest_task.lua
│ └── prefabs/
│ ├── crystal_tree.lua
│ ├── crystal_rock.lua
│ └── crystal_deer.lua
└── anim/
├── crystal_ground.zip
├── crystal_tree.zip
└── crystal_deer.zip
Step 1: Setting Up the Mod
First, let's create the basic mod structure and files:
modinfo.lua
name = "Crystal Forest Biome"
description = "Adds a beautiful Crystal Forest biome with unique resources and creatures."
author = "Your Name"
version = "1.0.0"
-- Compatible with Don't Starve Together
dst_compatible = true
dont_starve_compatible = false
reign_of_giants_compatible = false
-- Tags to help users find the mod
all_clients_require_mod = true
client_only_mod = false
-- Icon and priority
icon_atlas = "modicon.xml"
icon = "modicon.tex"
-- Mod configuration options
configuration_options = {
{
name = "BIOME_SIZE",
label = "Biome Size",
options = {
{description = "Small", data = 0.5},
{description = "Medium", data = 1.0},
{description = "Large", data = 1.5}
},
default = 1.0
},
{
name = "CRYSTAL_ABUNDANCE",
label = "Crystal Abundance",
options = {
{description = "Sparse", data = 0.5},
{description = "Normal", data = 1.0},
{description = "Abundant", data = 1.5}
},
default = 1.0
}
}
modmain.lua
-- Assets to preload
Assets = {
-- Ground textures
Asset("IMAGE", "levels/textures/crystal_ground.tex"),
Asset("IMAGE", "levels/textures/crystal_noise.tex"),
-- Minimap icons
Asset("IMAGE", "minimap/crystal_tree.tex"),
Asset("ATLAS", "minimap/crystal_tree.xml"),
Asset("IMAGE", "minimap/crystal_rock.tex"),
Asset("ATLAS", "minimap/crystal_rock.xml"),
}
-- Prefabs to register
PrefabFiles = {
"crystal_tree",
"crystal_rock",
"crystal_deer",
}
-- Configuration
local BIOME_SIZE = GetModConfigData("BIOME_SIZE")
local CRYSTAL_ABUNDANCE = GetModConfigData("CRYSTAL_ABUNDANCE")
-- Add the new ground type
local GROUND = GLOBAL.GROUND
local GROUND_NAMES = GLOBAL.STRINGS.NAMES.GROUND
local GROUND_TILES = GLOBAL.GROUND_TILES
-- Register new ground type
GROUND.CRYSTAL = #GROUND_TILES + 1
GROUND_NAMES.CRYSTAL = "Crystal Ground"
GROUND_TILES[GROUND.CRYSTAL] = "crystal_ground"
-- Add the ground assets
AddGamePostInit(function()
local GroundAtlas = GLOBAL.resolvefilepath("levels/textures/ground_noise.xml")
local GroundImage = GLOBAL.resolvefilepath("levels/textures/ground_noise.tex")
-- Add our custom ground
GLOBAL.TheWorld.components.groundcreep:AddGroundDef(
GROUND.CRYSTAL,
GroundAtlas,
GroundImage,
"levels/textures/crystal_noise.tex",
"crystal_ground"
)
end)
-- Add the custom rooms and tasks to world generation
modimport("scripts/map/rooms/crystal_forest.lua")
modimport("scripts/map/tasks/crystal_forest_task.lua")
-- Register our custom task with the world generation
AddLevelPreInitAny(function(level)
if level.location == "forest" then
-- Add our task to the level
table.insert(level.tasks, "crystal_forest_task")
-- Adjust task distribution based on mod config
level.overrides = level.overrides or {}
level.overrides.task_distribute = level.overrides.task_distribute or {}
level.overrides.task_distribute.crystal_forest_task = BIOME_SIZE
-- Adjust resource distribution
level.overrides.crystal_tree = CRYSTAL_ABUNDANCE
level.overrides.crystal_rock = CRYSTAL_ABUNDANCE
end
end)
-- Add our custom ground to the tile physics
AddSimPostInit(function()
for k, v in pairs(GLOBAL.GROUND_FLOORING) do
if v == GROUND.CRYSTAL then
GLOBAL.SetGroundFertility(v, 0)
GLOBAL.SetGroundClass(v, "rocky")
GLOBAL.SetGroundSpeedMultiplier(v, 1.2)
end
end
end)
Step 2: Creating the Biome Rooms
Now let's define the rooms that will make up our Crystal Forest biome:
scripts/map/rooms/crystal_forest.lua
require "map/room_functions"
-- Define the Crystal Forest clearing room
AddRoom("CrystalForest_Clearing", {
colour = {r=0.5, g=0.7, b=0.9, a=0.9},
value = WORLD_TILES.CRYSTAL,
tags = {"ExitPiece", "Crystal"},
contents = {
distributepercent = 0.12,
distributeprefabs = {
crystal_tree = 0.3,
crystal_rock = 0.2,
flint = 0.05,
rocks = 0.05,
evergreen = 0.1,
grass = 0.05,
sapling = 0.1,
flower = 0.05,
}
}
})
-- Define the Crystal Forest dense room
AddRoom("CrystalForest_Dense", {
colour = {r=0.4, g=0.6, b=0.8, a=0.9},
value = WORLD_TILES.CRYSTAL,
tags = {"Crystal"},
contents = {
distributepercent = 0.25,
distributeprefabs = {
crystal_tree = 0.5,
crystal_rock = 0.3,
flint = 0.03,
rocks = 0.03,
evergreen = 0.05,
grass = 0.03,
sapling = 0.05,
}
}
})
-- Define the Crystal Forest glade room
AddRoom("CrystalForest_Glade", {
colour = {r=0.6, g=0.8, b=1.0, a=0.9},
value = WORLD_TILES.CRYSTAL,
tags = {"Crystal"},
contents = {
distributepercent = 0.07,
distributeprefabs = {
crystal_tree = 0.1,
crystal_rock = 0.1,
flower = 0.2,
grass = 0.1,
sapling = 0.1,
carrot_planted = 0.05,
crystal_deer = 0.03,
}
}
})
Step 3: Creating the Biome Task
Now let's define the task that will incorporate our rooms into the world generation:
scripts/map/tasks/crystal_forest_task.lua
require "map/tasks"
-- Create a new task for the Crystal Forest biome
AddTask("crystal_forest_task", {
locks = {LOCKS.NONE},
keys_given = {KEYS.TIER1},
room_choices = {
["CrystalForest_Clearing"] = 2,
["CrystalForest_Dense"] = {2, 3},
["CrystalForest_Glade"] = {1, 2},
},
room_bg = WORLD_TILES.CRYSTAL,
background_room = "BGCrystal",
colour = {r=0.5, g=0.7, b=0.9, a=0.9}
})
-- Create a background room for the biome
AddRoom("BGCrystal", {
colour = {r=0.5, g=0.7, b=0.9, a=0.9},
value = WORLD_TILES.CRYSTAL,
tags = {"Crystal", "RoadPoison"},
contents = {
distributepercent = 0.05,
distributeprefabs = {
crystal_tree = 0.3,
crystal_rock = 0.2,
flint = 0.05,
rocks = 0.05,
grass = 0.05,
sapling = 0.05,
}
}
})
Step 4: Creating Custom Prefabs
Now let's create the custom prefabs for our biome:
scripts/prefabs/crystal_tree.lua
local assets = {
Asset("ANIM", "anim/crystal_tree.zip"),
Asset("MINIMAP_IMAGE", "crystal_tree"),
}
local prefabs = {
"crystal_shard",
"twigs",
"log",
}
local function OnChopDown(inst, chopper)
inst.SoundEmitter:PlaySound("dontstarve/wilson/use_axe_tree")
inst.AnimState:PlayAnimation("fall")
inst:ListenForEvent("animover", function()
local crystal = SpawnPrefab("crystal_shard")
crystal.Transform:SetPosition(inst.Transform:GetWorldPosition())
inst:Remove()
end)
end
local function OnChop(inst, chopper, chops)
inst.AnimState:PlayAnimation("chop")
inst.AnimState:PushAnimation("idle", true)
inst.SoundEmitter:PlaySound("dontstarve/wilson/use_axe_tree")
end
local function fn()
local inst = CreateEntity()
inst.entity:AddTransform()
inst.entity:AddAnimState()
inst.entity:AddSoundEmitter()
inst.entity:AddNetwork()
inst.entity:AddMiniMapEntity()
inst.MiniMapEntity:SetIcon("crystal_tree.tex")
MakeObstaclePhysics(inst, 0.25)
inst.AnimState:SetBank("crystal_tree")
inst.AnimState:SetBuild("crystal_tree")
inst.AnimState:PlayAnimation("idle", true)
inst:AddTag("tree")
inst:AddTag("crystal")
inst:AddTag("workable")
inst.entity:SetPristine()
if not TheWorld.ismastersim then
return inst
end
inst:AddComponent("inspectable")
inst:AddComponent("workable")
inst.components.workable:SetWorkAction(ACTIONS.CHOP)
inst.components.workable:SetWorkLeft(3)
inst.components.workable:SetOnFinishCallback(OnChopDown)
inst.components.workable:SetOnWorkCallback(OnChop)
inst:AddComponent("lootdropper")
inst.components.lootdropper:AddRandomLoot("crystal_shard", 1.0)
inst.components.lootdropper:AddRandomLoot("twigs", 0.5)
inst.components.lootdropper:AddRandomLoot("log", 0.25)
inst.components.lootdropper:SetLootSetupFn(function(inst)
inst.components.lootdropper.numrandomloot = math.random(1, 3)
end)
MakeHauntableWorkAndIgnite(inst)
-- Add light
inst:AddComponent("lighttweener")
inst.components.lighttweener:StartTween(
inst.entity:AddLight(),
0, -- start_radius
0.8, -- end_radius
nil, -- start_intensity
0.6, -- end_intensity
nil, -- start_falloff
nil, -- end_falloff
nil, -- duration
{1/255*130, 1/255*200, 1/255*255} -- color
)
return inst
end
return Prefab("crystal_tree", fn, assets, prefabs)
scripts/prefabs/crystal_rock.lua
local assets = {
Asset("ANIM", "anim/crystal_rock.zip"),
Asset("MINIMAP_IMAGE", "crystal_rock"),
}
local prefabs = {
"crystal_shard",
"rocks",
}
local function OnMined(inst, worker, workleft)
if workleft <= 0 then
inst.components.lootdropper:DropLoot()
SpawnPrefab("rock_break_fx").Transform:SetPosition(inst.Transform:GetWorldPosition())
inst:Remove()
else
inst.AnimState:PlayAnimation("hit")
inst.AnimState:PushAnimation("idle")
end
end
local function fn()
local inst = CreateEntity()
inst.entity:AddTransform()
inst.entity:AddAnimState()
inst.entity:AddSoundEmitter()
inst.entity:AddNetwork()
inst.entity:AddMiniMapEntity()
inst.MiniMapEntity:SetIcon("crystal_rock.tex")
MakeObstaclePhysics(inst, 0.5)
inst.AnimState:SetBank("crystal_rock")
inst.AnimState:SetBuild("crystal_rock")
inst.AnimState:PlayAnimation("idle")
inst:AddTag("boulder")
inst:AddTag("crystal")
inst.entity:SetPristine()
if not TheWorld.ismastersim then
return inst
end
inst:AddComponent("inspectable")
inst:AddComponent("workable")
inst.components.workable:SetWorkAction(ACTIONS.MINE)
inst.components.workable:SetWorkLeft(3)
inst.components.workable:SetOnWorkCallback(OnMined)
inst:AddComponent("lootdropper")
inst.components.lootdropper:AddRandomLoot("crystal_shard", 1.0)
inst.components.lootdropper:AddRandomLoot("rocks", 0.5)
inst.components.lootdropper:SetLootSetupFn(function(inst)
inst.components.lootdropper.numrandomloot = math.random(1, 3)
end)
MakeHauntableWorkAndIgnite(inst)
-- Add light
inst:AddComponent("lighttweener")
inst.components.lighttweener:StartTween(
inst.entity:AddLight(),
0, -- start_radius
0.6, -- end_radius
nil, -- start_intensity
0.5, -- end_intensity
nil, -- start_falloff
nil, -- end_falloff
nil, -- duration
{1/255*130, 1/255*200, 1/255*255} -- color
)
return inst
end
return Prefab("crystal_rock", fn, assets, prefabs)
scripts/prefabs/crystal_deer.lua
local assets = {
Asset("ANIM", "anim/crystal_deer.zip"),
}
local prefabs = {
"crystal_shard",
"meat",
}
local brain = require "brains/crystal_deer_brain"
local function OnAttacked(inst, data)
inst.components.combat:SetTarget(data.attacker)
inst.components.combat:ShareTarget(data.attacker, 30, function(dude)
return dude:HasTag("crystal_deer") and not dude:HasTag("player")
end, 5)
end
local function fn()
local inst = CreateEntity()
inst.entity:AddTransform()
inst.entity:AddAnimState()
inst.entity:AddSoundEmitter()
inst.entity:AddDynamicShadow()
inst.entity:AddNetwork()
MakeCharacterPhysics(inst, 100, 0.5)
inst.DynamicShadow:SetSize(1.5, 0.75)
inst.Transform:SetSixFaced()
inst.AnimState:SetBank("crystal_deer")
inst.AnimState:SetBuild("crystal_deer")
inst.AnimState:PlayAnimation("idle", true)
inst:AddTag("animal")
inst:AddTag("crystal_deer")
inst:AddTag("crystal")
inst.entity:SetPristine()
if not TheWorld.ismastersim then
return inst
end
inst:AddComponent("inspectable")
inst:AddComponent("health")
inst.components.health:SetMaxHealth(150)
inst:AddComponent("combat")
inst.components.combat:SetDefaultDamage(20)
inst.components.combat:SetAttackPeriod(2)
inst.components.combat:SetRetargetFunction(3, function(inst)
return FindEntity(
inst,
20,
function(guy)
return inst.components.combat:CanTarget(guy)
end,
nil,
nil,
{"crystal", "wall"}
)
end)
inst:AddComponent("lootdropper")
inst.components.lootdropper:AddRandomLoot("meat", 1.0)
inst.components.lootdropper:AddRandomLoot("crystal_shard", 1.0)
inst.components.lootdropper:SetLootSetupFn(function(inst)
inst.components.lootdropper.numrandomloot = math.random(1, 3)
end)
inst:AddComponent("locomotor")
inst.components.locomotor.walkspeed = 4
inst.components.locomotor.runspeed = 7
inst:SetStateGraph("SGdeer")
inst:SetBrain(brain)
inst:ListenForEvent("attacked", OnAttacked)
-- Add light
inst:AddComponent("lighttweener")
inst.components.lighttweener:StartTween(
inst.entity:AddLight(),
0, -- start_radius
0.7, -- end_radius
nil, -- start_intensity
0.5, -- end_intensity
nil, -- start_falloff
nil, -- end_falloff
nil, -- duration
{1/255*130, 1/255*200, 1/255*255} -- color
)
MakeHauntablePanic(inst)
return inst
end
return Prefab("crystal_deer", fn, assets, prefabs)
Step 5: Creating the Crystal Shard Item
Now let's create the crystal shard item that will be harvested from our biome:
scripts/prefabs/crystal_shard.lua
local assets = {
Asset("ANIM", "anim/crystal_shard.zip"),
Asset("ATLAS", "images/inventoryimages/crystal_shard.xml"),
Asset("IMAGE", "images/inventoryimages/crystal_shard.tex"),
}
local function light_on(inst)
if not inst.components.inventoryitem:IsHeld() then
inst.components.lighttweener:StartTween(
inst.entity:AddLight(),
0, -- start_radius
0.5, -- end_radius
nil, -- start_intensity
0.5, -- end_intensity
nil, -- start_falloff
nil, -- end_falloff
nil, -- duration
{1/255*130, 1/255*200, 1/255*255} -- color
)
end
end
local function light_off(inst)
inst.components.lighttweener:StartTween(
inst.entity:AddLight(),
0.5, -- start_radius
0, -- end_radius
0.5, -- start_intensity
0, -- end_intensity
nil, -- start_falloff
nil, -- end_falloff
nil, -- duration
{1/255*130, 1/255*200, 1/255*255} -- color
)
end
local function OnDropped(inst)
light_on(inst)
end
local function OnPickup(inst)
light_off(inst)
end
local function fn()
local inst = CreateEntity()
inst.entity:AddTransform()
inst.entity:AddAnimState()
inst.entity:AddNetwork()
MakeInventoryPhysics(inst)
inst.AnimState:SetBank("crystal_shard")
inst.AnimState:SetBuild("crystal_shard")
inst.AnimState:PlayAnimation("idle")
inst:AddTag("crystal")
inst:AddTag("molebait")
inst:AddTag("shiny")
inst.entity:SetPristine()
if not TheWorld.ismastersim then
return inst
end
inst:AddComponent("inspectable")
inst:AddComponent("inventoryitem")
inst.components.inventoryitem.atlasname = "images/inventoryimages/crystal_shard.xml"
inst.components.inventoryitem:SetOnDroppedFn(OnDropped)
inst.components.inventoryitem:SetOnPickupFn(OnPickup)
inst:AddComponent("stackable")
inst.components.stackable.maxsize = 40
inst:AddComponent("lighttweener")
light_on(inst)
-- Add recipe for crystal tools
MakeHauntableLaunch(inst)
return inst
end
return Prefab("crystal_shard", fn, assets)
Step 6: Creating the Ground Textures
For the biome to have a unique appearance, we need to create custom ground textures:
- Create
levels/textures/crystal_ground.tex
- This is the base texture for the ground - Create
levels/textures/crystal_noise.tex
- This is the noise texture that adds variation
You'll need to use an image editor to create these textures, then convert them to the game's format using the Don't Starve Tools.
Step 7: Creating the Crystal Deer Brain
Let's create a simple AI for our crystal deer:
scripts/brains/crystal_deer_brain.lua
require "behaviours/wander"
require "behaviours/runaway"
require "behaviours/doaction"
require "behaviours/panic"
local CrystalDeerBrain = Class(Brain, function(self, inst)
Brain._ctor(self, inst)
end)
local MIN_FOLLOW_DIST = 5
local TARGET_FOLLOW_DIST = 7
local MAX_FOLLOW_DIST = 10
local WANDER_DIST = 20
local function GetFaceTargetFn(inst)
return inst.components.combat.target
end
local function KeepFaceTargetFn(inst, target)
return inst.components.combat:TargetIs(target) and
inst.components.combat:InCooldown()
end
function CrystalDeerBrain:OnStart()
local root = PriorityNode(
{
WhileNode(function() return self.inst.components.health.takingfiredamage end, "OnFire", Panic(self.inst)),
ChaseAndAttack(self.inst, 10),
RunAway(self.inst, "player", MIN_FOLLOW_DIST, TARGET_FOLLOW_DIST, function(hunter)
return hunter:HasTag("player") and not hunter:HasTag("notarget")
end, nil, true),
FaceEntity(self.inst, GetFaceTargetFn, KeepFaceTargetFn),
Wander(self.inst, function()
local pos = self.inst:GetPosition()
return pos
end, WANDER_DIST)
}, .25)
self.bt = BT(self.inst, root)
end
return CrystalDeerBrain
Step 8: Adding Recipes for Crystal Items
Let's add some recipes to make use of the crystal shards:
Add to modmain.lua
-- Add recipes for crystal items
local crystal_axe = Recipe("crystal_axe",
{Ingredient("twigs", 2), Ingredient("crystal_shard", 3)},
RECIPETABS.TOOLS,
TECH.SCIENCE_ONE)
crystal_axe.atlas = "images/inventoryimages/crystal_axe.xml"
local crystal_pickaxe = Recipe("crystal_pickaxe",
{Ingredient("twigs", 2), Ingredient("crystal_shard", 3)},
RECIPETABS.TOOLS,
TECH.SCIENCE_ONE)
crystal_pickaxe.atlas = "images/inventoryimages/crystal_pickaxe.xml"
local crystal_spear = Recipe("crystal_spear",
{Ingredient("twigs", 2), Ingredient("crystal_shard", 2), Ingredient("rope", 1)},
RECIPETABS.WAR,
TECH.SCIENCE_ONE)
crystal_spear.atlas = "images/inventoryimages/crystal_spear.xml"
Step 9: Testing and Debugging
To test your mod:
- Place your mod folder in the Don't Starve Together mods directory
- Enable the mod in the game's mod menu
- Start a new game and explore to find your Crystal Forest biome
- Test the various features of your biome
Common Issues and Solutions:
- Biome Not Generating: Check your task and room definitions
- Missing Textures: Ensure all texture files are in the correct format and location
- Prefab Errors: Check for syntax errors in your prefab definitions
- AI Issues: Debug your brain logic if creatures behave unexpectedly
Step 10: Publishing Your Mod
Once your mod is working correctly, you can publish it to the Steam Workshop:
- Create a
modicon.tex
andmodicon.xml
(512x512 pixels) for your mod - Update your modinfo.lua with a detailed description
- Use the in-game mod uploader or the Don't Starve Mod Tools on Steam
- Provide clear instructions and screenshots in your Workshop description
Extending the Mod
Here are some ideas for extending this mod:
- Add more crystal-themed creatures
- Create special weather effects in the biome
- Add unique gameplay mechanics tied to the crystal theme
- Create a crystal-themed boss
- Add special events that only occur in the Crystal Forest
Conclusion
Congratulations! You've created a complete mod that adds a new biome to Don't Starve Together. This project demonstrates many important modding concepts:
- World generation and room creation
- Custom ground textures
- Creating unique resources and creatures
- AI programming
- Integrating with the existing game systems
Use what you've learned here as a foundation for creating more complex world modifications in the future!