Jump to content

Recommended Posts

Hello everyone! I've been meaning to create a few tutorials but since this one has been in demand I figured I'll make a tutorial about it. I will be making a dark haunted forest so you can follow along with me.

Pre-requisites

Spoiler
  • An IDE:

           Notepad++, VS Code, etc. I personally use VS Code, and will be using this IDE in the tutorial.

  • Texture and Atlas Packer:

           This is a texture and atlas packer to pack together dug up turf images neatly if you have multiple custom turfs.

           This is required to create your turfs, and any custom biome objects.

  • Image Editor Software:

           Krita, Fire Alpaca, and Paint tool SAI are the best ones to use to avoid white artifacts around sprites. But you can still use photoshop, Clip Studio Paint, and whichever favorite program you prefer.

  • Up to date Spriter or Spine2d:

           I highly recommend that you download the free up to date spriter from brashmonkey's website from the spriter pro tab instead of using the severely outdated one in the klei's modtools. Do not use modtools spriter. It is buggy, and will crash violently if you look at it even slightly funny. 

        Use DST MOD TOOL if you are using spine2d

  • Dst mod tool also doubles as a texpacker and I'll even recommend it for decompiling and compiling files including spriters. Grab it at the link below.

       https://msisunny.github.io/dst-mod-tool-publisher/
      

           Download the latest version Tiled. We're going to use the program to make setpieces.

Creating Tile Textures

Spoiler

Creating a custom floor tiles is extremely easy. But they come in two forms. 

Tiles: Tiles are bunched together to create a tileset that defines the edge of your custom turf. Each tile in a tileset is 128x128px squares. Tilesets are often reused by klei, and is overlaid with a texture to create variety without needing to redraw entire similar tilesets over and over again. When we create one of these, we want our tilesets to be white, so the texture's colors aren't disturbed. This is because DST blends these together ingame similar to using multiply or overlay setting in your image editor program. But in our case we're going to burrow klei's, which I'll explain how when we create out tile.lua.

Tilesets look like this:

image.png.769fa5099f9706393a2921f525bea2bc.png

This is an example of one of my custom tilesets. Again, normally you'll want this to be white with a greyish lineart, but on special occasions you can get away with coloring them. But we won't be using this one in the tutorial.

Textures: Textures are the pride and joy of every unique biome. In most occasions you don't really need a tileset to make your biome unique. Making a tileset is just a flex to show you're willing to go all out to make your biome more extra. In most cases you only really need a texture and can get away with just reusing the existing tilesets in DSTs data/levels/tiles folder. 

To create a texture, you make a new file with 1024x1024px dimensions in the image editor of your preference. In your program you then must create a pattern, it can be anything and then make it into a repeating texture. I will not go into the full details on how to create a repeating texture, because there are 1 million tutorial videos on youtube that show step to step processes on how you can do this, and some of them are less than 5 minutes long. Check those out! But in the end you should have a texture that looks like this.

image.png.da5080440f504c5ad0c9dd0e381a65d7.png

You may notice that this texture is a eye burning green color. Well that's on purpose because DST has a tendency to desaturate color to the extreme. So not only do you have to be wary about the tileset colors, but you also have to be aware of the lighting in DST. Otherwise your turf's texture can easily be ruined. I won't be using this texture for my darkforest, so don't panic if my tile texture is different in my ingame screenshots.

Name it noise_insertyournamehere and convert it into a tex file using textool.

Create a mini version that is 512x512px, half the size and name it mini_noise_insertyournamehere. I believe this is the version that will show when you view your map.

Placing your tiles and textures in the right folders:

Assuming that you have created all necessary folders for your mod to function. You next need to create a levels folder.

image.png.297bff52558d81d9bf942953e6551c42.png

Inside of the levels folder create a textures, and tiles folder. In our case we will leave the tiles folder empty, but drag and drop our new ground_noise_insertyournamehere.tex and mini_noise_insertyournamehere.tex inside of our textures folder.

image.png.5ed94e46a2f3af4d138bd3e4d2b92c10.png

I named mine evilforest. Otherwise you are done!

Creating your Tile.lua

Spoiler

So now that you have your textures set up, it's time to move on to the next step, and that's making the game recognize your tile's existence!

Now everyone has different locations they like to place their files, but as a neat freak we're going to organize our entire mod around how I do it.

We're going to make a init folder and create a init_tiles.lua script inside.

image.png.95ac0f2d13675332fb5631aced5c5976.png

We now open up this new file. You can copy and paste this as a template into your own file, as I explain what each of these do.

local NOISES = require("noisetilefunctions") --- If we want our biomes to look interesting and not bland. You're going to need to require "noisetilefunctions"
local ChangeTileRenderOrder = ChangeTileRenderOrder --- Tile orders is what determines which tileset is overlapping the other

local EVILFOREST_COLOR = --- this is the color of our biome's ocean. Change all instances of the name Evilforest into what you wish to name your biome. Please take note that the colors uses rgba code. Red, Green, Blue, and Alpha. In my code below, I favor the color green, change this to whichever color is to your liking as long as it doesn't exceed 255.
{
	primary_color =        {15, 45, 5,  255}, -- {153, 76, 0,  200}, --- color during the day
	secondary_color =      {20,  20, 5, 200}, -- {102,  51, 0, 255/2}, --- 
	secondary_color_dusk = {0,  10, 2, 125}, -- {51,  25, 0, 80}, --- color it changes to during dusk
	minimap_color =        {3,  22,  13,  150}, --- Your minimap tile colors
}


local EVILFOREST_WAVETINTS = --- this is the color of your ocean's waves.
{
    evilforest = {49, 240, 180} -- 1,  0.20,   0.10
}

AddTile("OCEAN_EVIL", "OCEAN", --- This is how we add our ocean tile. The first part is the name of our tile, and the second part is telling the game it's an ocean tile.
    {
		ground_name = "Evil Waves", --- I think this is just it's general name ingame
	},
    {
        name = "cave", --- Uses cave tileset.
        noise_texture = "levels/textures/ocean_noise.tex", --- This tile is using the ocean_noise.tex found in data/levels/texture/ocean_noise.tex
        runsound="dontstarve/movement/run_marsh", --- The sound the tile makes if you happen to walk on it
        walksound="dontstarve/movement/walk_marsh",
        snowsound="dontstarve/movement/run_ice",
        mudsound = "dontstarve/movement/run_mud",
        ocean_depth = "SHALLOW", --- This is how deep you want your ocean. In this case this is shallow water.
        colors = EVILFOREST_COLOR, --- We're grabbing the ocean's colors
        wavetint = EVILFOREST_WAVETINTS.evilforest, --- We're grabbing the oceans wave tints.
    },
    {
        name = "map_edge", --- I don't know. Do map tiles have names?
        noise_texture = "levels/textures/mini_water_coral.tex", --- How it looks on the map
    }
)

AddTile("EVILFOREST", "LAND", --- Same as the ocean tile, except we tell the game this is a land tile.
	{
		ground_name 	= "Evil Ground",  
	},
	{
		name			= "grass3", --- This is the tileset we're using. If we had a custom one, we would use that.
		noise_texture	= "levels/textures/ground_noise_evilforest.tex", --- We grab our custom texture and jot it down here!
		runsound 		= "dontstarve/movement/run_dirt",
        walksound 		= "dontstarve/movement/walk_dirt",
		snowsound		= "dontstarve/movement/run_snow",
		mudsound        = "dontstarve/movement/run_mud",
		colors = EVILFOREST_COLOR, --- the color of the surrounding water around this turf. In this case our evilforest ocean.
	},
	{
		name 			= "map_edge",
		noise_texture	= "levels/textures/mini_noise_evilforest.tex", --- We grab custom our map texture
        pickupsound = "grainy", --- When you pick stuff up from this turf it sounds sandy
	}

)


AddTile("EVILFOREST_NOISE", "NOISE") --- This is our noises tiles and why we need to require noisetilefunctions. If we want a more interesting look, we can have our new turf mix with other tiles. In this case, I want to mix my evilforest tiles with some dirt tiles so we don't walk into my new forest and it's just flat evilforest turf.

local function GetTileForEvilforestNoise(noise) --- based on this code, we will get 50% of each of these tiles. If you want more dirt or more of your custom tile. You can tweak the .5, or you can go absolutely nuts and slap a math.random in there so you create some crazy noise.
    return noise < .5 and WORLD_TILES.EVILFOREST or WORLD_TILES.DIRT
end

NOISES[WORLD_TILES.EVILFOREST_NOISE] = GetTileForEvilforestNoise

--- Below is just the code which tells the game which tile appears above the other
ChangeTileRenderOrder(WORLD_TILES.OCEAN_EVIL, WORLD_TILES.OCEAN_HAZARDOUS, true)

ChangeTileRenderOrder(WORLD_TILES.EVILFOREST, WORLD_TILES.DIRT)

And that is how you set up your tiles.

Creating Rooms and Tasks

Spoiler

Now that you have your tiles set up. It's time to create your rooms. Rooms are what will determine what spawns inside your biomes, and tasks is the file that will combine your rooms together to create your entire biome!

So let's set up your folders for your rooms, and tasks. Head over to your scripts folder, and create a new map folder inside. After wards make two new folders called rooms, and tasks inside of your map folder. It should look something like this.

image.png.7bf4e2900a127bdf9a5f7b304b8f63e9.png

Once you've done this, create a new lua script called insertname_rooms.lua inside the rooms folder. I'm going to name mine evilforest_rooms.lua.

Open your new rooms lua and copy and paste the code below. Replacing the necessary information if needed.

  

	AddRoom("BG EvilForest",  { --- I don't fully understand what BG rooms are, but I add them anyway.
		colour={r=0.3,g=0.2,b=0.1,a=0.3}, --- color of this room
		value = WORLD_TILES.EVILFOREST_NOISE, --- We grab our custom tiles! This being the noise tile we made which is a mix of dirt and our custom turf
		contents = {
			distributepercent = 0.07, --- This is the distrubute percent. I'm bad at math so you'll have to play with this
			distributeprefabs = --- These are all of the prefabs that will appear in your biome
			{
				marsh_tree = 1,
				marsh_bush = 0.66,

			},
		}
	})

	AddRoom("EvilForest",  { --- This is one of your main rooms. We will add this.
		colour={r=0.3,g=0.2,b=0.1,a=0.3},
		value = WORLD_TILES.EVILFOREST_NOISE,
		contents = {
			countstaticlayouts={["skeleton_camper"] = 1}, --- If you wish to put setpieces in your biomes, you use countstaticlayouts. I grabbed a random dead guy setpiece from data/databundles/scripts/map/static_layouts for this one
			countprefabs= {
			burntground_faded = function() return math.random(3,5) end, --- countprefabs aree garenteed prefabs that will spawn in your biome. You can mix the amount with math.random or use a flat number. In this case I put meteorspawners here with a chance of 3 to 5 of them generating in the world.
			},
			distributepercent = 0.07,
			distributeprefabs =
			{
				marsh_tree = 1,
				marsh_bush = 0.66,
				fireflies = 0.33,
				evergreen_sparse = 6,
			},
		},
		
	})
	
	
	AddRoom("EvilForest Ponds",  { --- We'll add a room that has a bunch of ponds for fun too
		colour={r=0.3,g=0.2,b=0.1,a=0.3},
		value = WORLD_TILES.EVILFOREST, --- Let's make this room only have evilforest turf. No mixing.
		contents = {			
			distributepercent = 0.1,
			distributeprefabs =
			{				
				pond_cave = 0.1,

				butterfly = .5,				
				flower = .3,
				evergreen_sparse = 3,
				notthebees = {weight = .2, prefabs = {"beehive", "wasphive"}},	--- If you'vee ever gotten worlds were you get nothing but twiggy trees or juicy berries. This is how you make it choose between two prefabs to spaw. In this case you might get bees or you might get wasps.			
				grass = 1,
				marsh_tree = 1,
				fireflies = 0.33,
				marsh_bush = 0.66,			
			},
		},
		
	})

You can make as many rooms as you wish, and mix up the tiles!

Now that your rooms are complete. It's time to move to creating your tasks which will bundle up all your rooms together to make your biome.

Open your newly created tasks folder and create a new lua script called insertnamehere.lua. I'm naming mine evilforest.lua.

It's time to open your new scripts file and follow along.

require("map/rooms/evilforest_rooms") --- we are going to grab our rooms, in my case it's my evilforest_rooms

AddTask("Evil Forest", { --- this is the name of our task
	locks={}, --- From my understanding only certain biomes with certain keys can attach to it. So If I put LOCKS.TIER4 only tasks that have a KEYS.TIER4 can attach to it.
	keys_given={KEYS.ISLAND_TIER2}, --- I think keys are biomes that it has permission to attach to
	region_id = "evilforest", 
	level_set_piece_blocker = true,
	room_tags = {"RoadPoison", "not_mainland"}, --- Road poison means roads won't spawn in your biome, and not mainland just means it's an island
	room_choices={ --- These are our rooms. We're spawning 2 duplicates of Evilforest and a Evilforest with the ponds. Doing this helps make the biome bigger, and helps you organize it's sub-biomes.
		["EvilForest"] = 2,
		["EvilForest Ponds"] = 1,			
	},
	room_bg=WORLD_TILES.OCEAN_EVIL, 
	background_room = "BG EvilForest",
	cove_room_name = "Empty_Cove", --- Your biome can spawn an empty cove inside of it
	make_loop = true, --- I don't know
	crosslink_factor = 2, --- I'm unsure
	cove_room_chance = 1, --- Only a chance of 1 cove can spawn
	cove_room_max_edges = 2,
	colour={r=.05,g=.5,b=.05,a=1},
})	

AddTask("Evil Land", { --- If you want your biome to be on the mainland here's how you do it
	locks={LOCKS.NONE},
	keys_given={KEYS.TIER2},
	region_id = "evilforest",
	level_set_piece_blocker = true,
	room_tags = {"RoadPoison", "evilforest"}, --- You can also add your own biome tags here
	room_choices={
		["EvilForest"] = 3,			
	},
	room_bg=WORLD_TILES.DIRT_NOISE,
	background_room = "BG EvilForest",
	colour={r=.05,g=.5,b=.05,a=1},
})

And we are all done with this.

Setting up modworldgenmain

Spoiler

Now that you have everything set up. It's time to put everything together so your biome spawns in the world! Let's create our modworldgenmain, but before we do so let's go back into our init folder and create a init_worldgen.lua file. You don't usually need to do this, but the modworldgenmain can become terribly cluttered easily, so let's make sure everything is neat.

Your init folder should look like this.

image.png.f40cc2cb74ae8f66d879bc37391c24e3.png

Once you create your new init_worldgen file. Open it and add this:

local ENV = env
GLOBAL.setfenv(1, GLOBAL)

local AddLevelPreInitAny = ENV.AddLevelPreInitAny
local AddTaskSetPreInitAny = ENV.AddTaskSetPreInitAny --- We're using this one
local AddRoomPreInit = ENV.AddRoomPreInit
local AddTaskPreInit = ENV.AddTaskPreInit
local GetModConfigData = ENV.GetModConfigData --- add this just in case you want to add configuration options later

local tasks = require("map/tasks/evilforest") --- We're grabbing our task here

local evilforest_tasks = {"Evil Forest", "Evil Land"}

AddTaskSetPreInitAny(function(tasksetdata)
   		if tasksetdata.location == "forest" and tasksetdata.tasks and #tasksetdata.tasks > 1 then --- Our biome should appear in a forest world. If you wish to add your biome to island adventures, you must make your biome check for their mod and it's location.
			for i, setpieces in ipairs(evilforest_tasks) do
			table.insert(tasksetdata.tasks, setpieces)
			end	
   		end
end)

Okay once you have made this, now we create our modworldgenmain.lua and open. You just need to copy and paste this code and you are done!

local ENV = env
GLOBAL.setfenv(1, GLOBAL)

--- modimporting is ann excellent way to keep your modmains clean
local modimport = ENV.modimport

modimport("init/init_tiles") --- We're grabbing our tiles

modimport("init/init_worldgen") --- We're grabbing our worldgen.

Testing Our New Biome

Spoiler

Loading in and finally your new biome should be working! Check it out.

image.png.54d3536f24d9ff16b49281c484959d26.png

You may have to mess around with your keys and locks; maybe delete them to get them connecting right. 

Adding Ambient Sound

Spoiler

 Now that our biome works. It's a little too quiet. Let's add some ambiance to it. Just like how we made an init folder, we're going to make a new folder called postinit. This is going to be the place where we add our patches to the game. In this case we're going to be patching a component called ambientsound.lua. Make a components folder in postinit, and create a new lua script named ambientsound.lua. 

It should look like this

image.png.b9194ece003a1b236ba6877bc3ca97cf.png

Copy and paste this code and replace any Evilforest you find with your tile's name.

local ENV = env
GLOBAL.setfenv(1, GLOBAL)

local EVILFOREST_AMBIENT_SOUND = {  
	[WORLD_TILES.EVILFOREST] = {sound = "dontstarve/AMB/forest", wintersound = "dontstarve/AMB/forest_winter", springsound = "dontstarve/AMB/forest", summersound = "dontstarve_DLC001/AMB/forest_summer", rainsound = "dontstarve/AMB/forest_rain"},	
	[WORLD_TILES.OCEAN_EVIL] = {sound = "turnoftides/together_amb/ocean/shallow", rainsound = "turnoftides/together_amb/ocean/shallow_rain"}
}

ENV.AddComponentPostInit("ambientsound", function(self)
	local AMBIENT_SOUNDS, SOUND = EvilforestUpvalue(self.OnUpdate, "AMBIENT_SOUNDS")
	
	if SOUND then
		for k, v in pairs(EVILFOREST_AMBIENT_SOUND) do
			AMBIENT_SOUNDS[k] = EVILFOREST_AMBIENT_SOUND[k]
		end
	end
end)

You can change these sounds and even create custom ones! Easiest way to use existing sounds is to check data/databundles/scripts/components/ambientsound.lua and grab the ones you think matches your biome.

We're not done yet. We have something called EvilforestUpvalue in here. It's an UpvalueHacker so we need to add this somewhere or the game will crash. We're going to make a new lua script called evilforest_util.lua. But you can name it something else. Slap it into your scripts folder so it looks like this. I also have very little understanding of UpvalueHackers, so don't ask me why we need it ^^;

image.png.8e0b5ecf491eb638d10cf61dc4a1e34f.png

Then open it to add this code

function EvilforestUpvalue(fn, upvalue_name, set_upvalue) ---Make sure this name matches the name in your ambientsound.lua, or whereever you add a UpvalueHacker.
	if fn == nil or upvalue_name == nil then
		return
	end
	
	local i = 1
	while true do
		local val, v = debug.getupvalue(fn, i)
		
		if not val then
			break
		end
		if val == upvalue_name then
			if set_upvalue then
				debug.setupvalue(fn, i, set_upvalue)
			end
			
			return v, i
		end
		i = i + 1
	end
end

 

Next we're going to go into our modmain.lua. Just copy and paste this code in and your biome should give off your desired ambience every time your character steps on it.

require("evilforest_util") --- name this whatever you named it

local components = {
	"ambientsound",
}

for _, v in pairs(components) do
	modimport("postinit/components/"..v)
end

  

Making turf items

Spoiler

 Unlike most inventory items, turfs are a little special. From this point on I'm going to assume you are an experienced enough modder who knows how to mod items into the game already without any problems. Since there exists item creation tutorials, I recommend newcomers to become familiar with creating items before continuing because we're going to be making a couple of new things that go straight into strange territory soon. I will show a process to bulk pack your inventory images as I go over these turfs because this is important when creating biomes with lots of custom things in it. You can skip these short sections if you're in the know though.

First you're going to need to draw up your turf. Once you're done, your spriter project should look like this.

image.png.68468c352c6aa721ea904a8af96e714e.png

Rename both your project and build to insertnamehere_turf, make sure it's a broad name because it'll leave room for other turfs you can add as animations if you decide to make more. Name your current animation which is highlighted under the build name on the lower right insertnamehere based off the tile you're working on. Remember these names. Afterwards we're going to compile this into a new insertnamehere_turf.zip. Mines named evil_turf in this instance, and we're going to go through the usual steps of sticking it's newly compiled zip in the anim folder.

We're then going to create a 64x64px version for it's inventory image named turf_insertnamehere. The turf prefix is important. In your images folder, create a inventoryimages folder if you haven't. This folder isn't that important if you aren't already using it and can be deleted if you decide to upload on steam. Instead it's going to be the place you store all of your pngs for all of your future 64x64px inventory item pictures.

image.png.994096a072d56dd970cf1a31c4ad8532.png

Open your Texture and Atlas packer then drag in your one singular 64x64 turf image on top of it from your inventoryimages folder into the packer. In the future you will be bulk dragging all of them inside the packer. Make sure to uncheck crop, and then pack it into your images folder outside inventoryimages.

image.png.4c808648b2c7c3451a22a98eea289d52.png

Name it something unspecific since you're going to put all kinds of inventory items in here from now on. I named mine evil_inventoryimages.

image.png.3966593569a478705e7f9628b8c9707a.png

Once you have done this, in your init folder; you're going to create a new script named init_assets. I will assume that most people will have their assets located in the modmain, and if you do, you can just add these necessary details inside of there directly however you want instead. But if you wish to be a bit organized you can create this script and keep the assets in here.

Assets = {
	Asset("IMAGE", "images/evil_inventoryimages.tex"),
	Asset("ATLAS", "images/evil_inventoryimages.xml"),
	Asset("ATLAS_BUILD", "images/evil_inventoryimages.xml", 256),
}

local ITEMS = {	
	"turf_evilforest", --- these are the inventory items from your packaged inventoryimages. You'll need to keep adding them in when making new items.
}

for i, v in pairs(ITEMS) do
	RegisterInventoryItemAtlas(GLOBAL.resolvefilepath("images/evil_inventoryimages.xml"), v..".tex")

Then add this code to your modmain so it recognizes it.

require("evilforest_util")
require("evilforest_strings") --- I put a require for my strings and is grabbing it from my scripts folder.

local inits = {
	"init_assets",
}

for _, v in pairs(inits) do --- we're modimporting init_assets. If you have existing assets you can put them in init_assets as well
	modimport("init/"..v)
end


local components = {
	"ambientsound",
}

for _, v in pairs(components) do
	modimport("postinit/components/"..v)
end

Once this is done. We're going to return to our init_tiles.lua, and add some new code to our tiles. Specifically to our newly created land tile.

AddTile("EVILFOREST", "LAND",
	{
		ground_name 	= "Evil Ground",
	},
	{
		name			= "grass3",
		noise_texture	= "levels/textures/ground_noise_evilforest.tex",
		runsound 		= "dontstarve/movement/run_dirt",
        walksound 		= "dontstarve/movement/walk_dirt",
		snowsound		= "dontstarve/movement/run_snow",
		mudsound        = "dontstarve/movement/run_mud",
		colors = EVILFOREST_COLOR,
	},
	{
		name 			= "map_edge",
		noise_texture	= "levels/textures/mini_noise_evilforest.tex",
        pickupsound = "grainy",
	},
	{--- this is the new code we're adding
		name = 			"evilforest", --- I want to name my turf evilforest
		anim = 			"evilforest", --- this is the animation name in my spriter file
		bank_build = 	"evil_turf", --- My spriter file's build and bank is called evil_turf
		pickupsound = 	"grainy",
	}

)

So unlike normal prefabs. The tile itself turns it into a prefab and puts the turf_ prefix in front of it. This is why calling your inventoryimage version turf_insertnamehere is important. Your animations, builds and banks are also defined inside your tiles whenever you're pitchforking up your turfs.

The final step is to just add your strings to describe, name and give a recipe description to your turf. I put my evilforest_strings in scripts right next to my evilforest_util.lua seen above in my modmain. But you can put them straight in modmain with the assets if you don't feel like requiring it. This is what my strings look like.

--- It's quick and messy and I do not recommend doing it like this if you're writing multiple characters. 
local NAMES = STRINGS.NAMES
local RECIPE_DESC = STRINGS.RECIPE_DESC
local DESCRIBE = STRINGS.CHARACTERS.GENERIC.DESCRIBE

--- Other
DESCRIBE.TURF_EVILFOREST = "It's a creepy tuft of grass"
NAMES.TURF_EVILFOREST = "DarkForest Turf"

---	Recipes
RECIPE_DESC.TURF_EVILFOREST = "A patch of disturbing grass."

And that's how you add turfs as an item. 

Setting up pillars and shade

Spoiler

Assuming you know how to create prefabs and items ingame. I'm going to show you how to create a tall pillar object that casts a custom shadow and animated leaves over the top of your screen in the next section.

When creating taller objects, the compiler is unable to handle certain image sizes. Pillars are over 2800px tall, so in order for you to compile such an image you much chop it into pieces, or the compiler will fail and the game will cull your pillar, creating nasty pop in every time you come near it. I cut my tree into two pieces, but you can chop it up further.

When you open your spriter project, you should make sure your pivot on the upper most half is located on the very top of the image in the scene. This will prevent pop in.

image.png.4a001bf8b7a8ad0e2ac878d271a0747b.png

Otherwise compile and treat your tall object like any other prop, item, or object in game. I compiled my tall tree and named both it's anim and its prefab eviltree_tall.

Let's set up our tree's prefab so it can start casting custom or existing shade and leaves. A heads up is to pick a keyword and stick with it, in my case it's eviltree, because we're going to be making a lot of files to link up, so to save yourself a headache name it the same. Let's start. Also if you wish to test shade without canopy, comment out or delete all code related to the playerprox component.

Also add inst:AddTag("shadecanopy") to Protect against wildfires, blizzards, etc. For mod compatibility sake. 

local assets = {
	Asset("ANIM", "anim/eviltree_tall.zip"),
	Asset("SCRIPT", "scripts/prefabs/eviltreeshadow.lua") --- We are going to create this script next, but you must put it into your pillar's assets. Rename it to keywordshadow.lua
}

local MIN = TUNING.SHADE_CANOPY_RANGE
local MAX = MIN + TUNING.WATERTREE_PILLAR_CANOPY_BUFFER

local function OnFar(inst, player)
    if player.canopytrees then   
        player.canopytrees = player.canopytrees - 1
        player:PushEvent("onchangeevilzone", player.canopytrees > 0) --- onchangeevilzone is a custom event and is what spawns our custom canopy leaves. Rename it to onchangekeywordzone.
    end
    inst.players[player] = nil
end

local function OnNear(inst,player)
    inst.players[player] = true

    player.canopytrees = (player.canopytrees or 0) + 1

    player:PushEvent("onchangeevilzone", player.canopytrees > 0)
end

local function OnRemoveEntity(inst)
    for player in pairs(inst.players) do
        if player:IsValid() then
            if player.canopytrees then
                player.canopytrees = player.canopytrees - 1
                player:PushEvent("onchangeevilzone", player.canopytrees > 0)
            end
        end
    end
end

local function fn()
	local inst = CreateEntity()
	
	inst.entity:AddTransform()
	inst.entity:AddAnimState()
	inst.entity:AddMiniMapEntity()
	inst.entity:AddNetwork()
	
	MakeObstaclePhysics(inst, 3)
	
	inst.MiniMapEntity:SetIcon("eviltree_tall.tex") 
	
	inst.AnimState:SetBank("eviltree_tall")
	inst.AnimState:SetBuild("eviltree_tall")
	inst.AnimState:PlayAnimation("idle")
	
	inst:AddTag("antlion_sinkhole_blocker")
	inst:AddTag("birdblocker")
	inst:AddTag("NOCLICK")
	
	if not TheNet:IsDedicated() then
    --- this is all the stuff that deals with your pillar casting shade.
		inst:AddComponent("distancefade")
		inst.components.distancefade:Setup(15, 25)
		
		inst:AddComponent("eviltreeshade") --- this is a custom component we're going to make. Name it keywordshade
		inst.components.eviltreeshade.range = math.floor(TUNING.SHADE_CANOPY_RANGE / 4) --- this is how big your shade is. Increase the 4 into a bigger number to make smaller shade.
	end
	
	inst.entity:SetPristine()
	
	if not TheWorld.ismastersim then
		return inst
	end

  --- the playerprox component is what causes the leafy canopy to appear above the player's screen. If you don't care about the canopy and only want shade, then you can remove everything in relation to this component, and skip the canopy section.
	inst.players = {}
    inst:AddComponent("playerprox")
	inst.components.playerprox:SetTargetMode(inst.components.playerprox.TargetModes.AllPlayers)
    inst.components.playerprox:SetDist(MIN, MAX)
    inst.components.playerprox:SetOnPlayerFar(OnFar)
    inst.components.playerprox:SetOnPlayerNear(OnNear)
	
	inst.OnEntitySleep = OnEntitySleep
	inst.OnEntityWake = OnEntityWake
	
	local scale = math.random() > 0.5 and 1 or -1
	inst.AnimState:SetScale(scale, 1)

	inst.OnRemoveEntity = OnRemoveEntity	
	return inst
end

return Prefab("eviltree_tall", fn, assets)

Next we're going to create a new script in prefabs. The eviltreeshadow.lua in my case and keywordshadow.lua for you. 

In your new prefab add this code and replace all instances of eviltree with your own keyword, it is case sensitive so capitalize your name exactly as I have done because these will be used in a component we're making next.

local function SpawnShadow(inst, range, no_lightrays)
	if not TheWorld.shadetiles then
		TheWorld.shadetiles = {}
	end
	if not TheWorld.shadetile_key_to_eviltree_shade_id then
		TheWorld.shadetile_key_to_eviltree_shade_id = {} --- replace eviltree with your keyword
	end
	
	local data = {lightrays = {}, shadetile_keys = {}}
	local x, y, z = inst.Transform:GetWorldPosition()
	
	for i = -range, range do
		for t = -range, range do
			if ((t * t) + (i * i)) <= range * range then
				local newx = (math.floor((x + (i * 4)) / 4) * 4) + 2
				local newz = (math.floor((z + (t * 4)) / 4) * 4) + 2
				
				local shadetile_key = newx.."-"..newz
				table.insert(data.shadetile_keys, shadetile_key)
				
				if not TheWorld.shadetiles[shadetile_key] or TheWorld.shadetiles[shadetile_key] <= 0 then
					if math.random() < 0.8 then
						TheWorld.shadetile_key_to_eviltree_shade_id[shadetile_key] = SpawnEvilTreeShade(newx, newz) --- make sure it's capitalized appropiately when replacing.
					elseif not no_lightrays then
						if math.random() < 0.5 then
							local rays = SpawnPrefab("lightrays_canopy") 
							
							rays.Transform:SetPosition(newx, 0, newz)
							table.insert(data.lightrays, rays)
						end
					end
					TheWorld.shadetiles[shadetile_key] = 1
				else
					TheWorld.shadetiles[shadetile_key] = TheWorld.shadetiles[shadetile_key] + 1
				end
			end
		end
	end
	
	return data
end

return {spawnshadow = SpawnShadow}

Next make a new components folder in your scripts, and create a new script named keywordshade.lua. Mines like seen in my tree prefab is eviltreeshade, and should look like this in your directory.

image.png.b9ffe6f0e32575bf3c1f8f6ecec1d5d4.pngBelow, replace all instances of eviltree with your keyword. Remember it is case sensitive so make sure you use the exact same cases.

local function GenerateAndSpawnEvilTreeShadePositions(inst)
	local self = inst.components.eviltreeshade
	if self == nil then
		return
	end
	
	self:GenerateEvilTreeShadePositions()
	self:SpawnShadows()
end

local EvilTreeShade = Class(function(self, inst)
	self.inst = inst
	
	self.range = math.floor(TUNING.SHADE_CANOPY_RANGE / 4)
	
	self.EvilTreeShade_positions = {}
	self.spawned = false
	
	inst:DoTaskInTime(0, GenerateAndSpawnEvilTreeShadePositions)
end)

function EvilTreeShade:OnRemoveEntity()
	self:DespawnShadows(true)
	self:RemoveEvilTreeShadePositions()
end

EvilTreeShade.OnRemoveFromEntity = EvilTreeShade.OnRemoveEntity

Global_EvilTreeShade = {}
local EvilTreeShades = Global_EvilTreeShade

function EvilTreeShade:GenerateEvilTreeShadePositions()
	local x, y, z = self.inst.Transform:GetWorldPosition()
	
	for i = -self.range, self.range do
		for t = -self.range, self.range do
			if math.random() < 0.8 and ((t * t) + (i * i)) <= self.range * self.range then
				local newx = math.floor((x + i * 4) / 4) * 4 + 2
				local newz = math.floor((z + t * 4) / 4) * 4 + 2
				
				local shadetile_key = newx.."-"..newz
				local shadetile = EvilTreeShades[shadetile_key]
				
				if not shadetile then
					table.insert(self.EvilTreeShade_positions, {newx, newz})
					EvilTreeShades[shadetile_key] = {refs = 1, spawnrefs = 0}
				else
					shadetile.refs = shadetile.refs + 1
				end
			end
		end
	end
end

function EvilTreeShade:RemoveEvilTreeShadePositions()
	for i, v in ipairs(self.EvilTreeShade_positions) do
		local x, z = v[1], v[2]
		
		local shadetile_key = x.."-"..z
		local shadetile = EvilTreeShades[shadetile_key]
		
		shadetile.refs = shadetile.refs - 1
		if shadetile.refs == 0 then
			EvilTreeShades[shadetile_key] = nil
		end
	end
end

function EvilTreeShade:OnEntitySleep()
	if not IsTableEmpty(self.EvilTreeShade_positions) then
		self:DespawnShadows()
	end
end

function EvilTreeShade:OnEntityWake()
	if not IsTableEmpty(self.EvilTreeShade_positions) then
		self:SpawnShadows()
	end
end

function EvilTreeShade:SpawnShadows()
	if self.spawned or not self.inst.entity:IsAwake() then
		return
	end
	
	for i, v in ipairs(self.EvilTreeShade_positions) do
		local x, z = v[1], v[2]
		local shadetile = EvilTreeShades[x.."-"..z]
		
		shadetile.spawnrefs = shadetile.spawnrefs + 1
		if shadetile.spawnrefs == 1 then
			shadetile.id = SpawnEvilTreeShade(x, z)
		end
	end
	
	self.spawned = true
end

function EvilTreeShade:DespawnShadows(ignore_entity_sleep)
	if not self.spawned or (not ignore_entity_sleep and self.inst.entity:IsAwake()) then
		return
	end
	
	for i, v in ipairs(self.EvilTreeShade_positions) do
		local x, z = v[1], v[2]
		local shadetile = EvilTreeShades[x.."-"..z]
		
		shadetile.spawnrefs = shadetile.spawnrefs - 1
		if shadetile.spawnrefs == 0 then
			DespawnEvilTreeShade(shadetile.id)
			shadetile.id = nil
		end
	end
	
	self.spawned = false
end

return EvilTreeShade

Afterwards, go into your posinit folder and create a new script called shadeeffects.lua then put in this code. The shadeeffects file is where you control every visual aspect about your shadow, down to how it even moves. So make sure to really play around in here to get the perfect look.

local ENV = env
GLOBAL.setfenv(1, GLOBAL)

if TheNet:IsDedicated() then
	local nullfunc = function() end
	
	SpawnEvilTreeShade = nullfunc
	DespawnEvilTreeShade = nullfunc
	ShadeRendererEnabled = nil
	
	return
end

ShadeTypes.EvilTreeShade = ShadeRenderer:CreateShadeType()

--- If you want to have shade that does not move, then turn eveery tuning value into 0
ShadeRenderer:SetShadeMaxRotation(ShadeTypes.EvilTreeShade, TUNING.CANOPY_MAX_ROTATION) 

ShadeRenderer:SetShadeRotationSpeed(ShadeTypes.EvilTreeShade, TUNING.CANOPY_ROTATION_SPEED) 

ShadeRenderer:SetShadeMaxTranslation(ShadeTypes.EvilTreeShade, TUNING.CANOPY_MAX_TRANSLATION) 

ShadeRenderer:SetShadeTranslationSpeed(ShadeTypes.EvilTreeShade, TUNING.CANOPY_TRANSLATION_SPEED) 

ShadeRenderer:SetShadeTexture(ShadeTypes.EvilTreeShade, resolvefilepath("images/tree.tex")) --- set this up so you can use custom tex images, but in this case I'm just grabbing the existing leaves texture from klei's image folder. You can make all kinds of patterns cast on the ground.

-- Messing around with the value of the 360 makes random rotations of the images less choatic if yourshadows move.
function SpawnEvilTreeShade(x, z)
	return ShadeRenderer:SpawnShade(ShadeTypes.EvilTreeShade, x, z, math.random() * 360, TUNING.CANOPY_SCALE)
end

function DespawnEvilTreeShade(id)
	ShadeRenderer:RemoveShade(ShadeTypes.EvilTreeShade, id)
end

local OldShadeEffectUpdate = ShadeEffectUpdate

function ShadeEffectUpdate(dt, ...)
	local r, g, b = TheSim:GetAmbientColour()
	
	ShadeRenderer:SetShadeStrength(ShadeTypes.EvilTreeShade, Lerp(0.3, 0.5, ((r + g + b) / 3) / 255)) --- this determines thee color and how dark your shadows are 
	return OldShadeEffectUpdate(dt, ...)
end

The the only remaining step is to get the game to recognize your shadows.

In your assets, whether you put it in modmain or in our tutorial's case in the init_assets folder. You need to add this into there.

Assets = {
	Asset("IMAGE", "images/evil_inventoryimages.tex"),
	Asset("ATLAS", "images/evil_inventoryimages.xml"),
	Asset("ATLAS_BUILD", "images/evil_inventoryimages.xml", 256),
}

local ITEMS = {	
	"turf_evilforest",
}

for i, v in pairs(ITEMS) do
	RegisterInventoryItemAtlas(GLOBAL.resolvefilepath("images/evil_inventoryimages.xml"), v..".tex")
end

AddSimPostInit(function() --- Adding this makes sure your custom texture for your shade loads in immediately. Otherwise it'll be reverted to using a vanilla shade, which we do not want.
	modimport("postinit/shadeeffects")
end)

There you have it, your pillar should now start casting shade!

Spawning custom canopies

Spoiler

Now it's time to create a custom canopy of leaves. In my mod I extracted the leaf_canopy.zip in data/anim then edited mine to not have flowers temporarily and named it evil_canopy. Put your new anim into your assets, mines Asset("ANIM", "anim/evil_canopy")

image.png.93944513d52c3f40fa4f87b2eb6a97ac.png

Then create a new widgets folder in your scripts folder. In this new widgets folder, we're going to create a new script called insertyournameherecanopy.lua. I named mine evilcanopy.lua.

Next add this code inside and replace any thing evil with your name.

local Widget = require "widgets/widget"
local Image = require "widgets/image"
local UIAnim = require "widgets/uianim"

local ROWS = 5
local ANIM_IDLES = 7
local POS_INDEX = {
    [1] = -5,
    [2] = -2.5,
    [3] = 0,
    [4] = 2.5,
    [5] = 5,
}

local function getxoffset()
    return (math.random()*400)-200
end

local ROW_ANIMS = {}

local function setnewanim(widget)
    local newnum = math.random(1,ANIM_IDLES)

    local row1 = widget.anim_row + 1
    local row2 = widget.anim_row - 1
    if row1 > ROWS then row1 = 1 end
    if row2 < 1 then row2 = ROWS end

    while newnum == ROW_ANIMS[row1][widget.anim_pos] or
          newnum == ROW_ANIMS[row2][widget.anim_pos] do
        newnum = math.random(1,ANIM_IDLES)
    end
    widget.animnum = newnum
    widget:GetAnimState():PlayAnimation("idle"..newnum,true)
    widget:GetAnimState():SetTime(math.random()*2)
	--- we don't have flowers since I saved blank transparent symbols over them, but I kept these flower symbols in just in case
    if math.random() < 0.3/5 then
        widget:GetAnimState():Show("flower01")
    end

    if math.random() < 0.3/5 then
        widget:GetAnimState():Show("flower02")
    end

    if math.random() < 0.3/5 then
        widget:GetAnimState():Show("flower03")
    end

    if math.random() < 0.3/5 then
        widget:GetAnimState():Show("flower04")
    end

    if math.random() < 0.3/5 then
        widget:GetAnimState():Show("flower05")
    end
end

local function addcanopyrow(widget,row)
    if not ROW_ANIMS[row] then
        ROW_ANIMS[row] = {}
    end
    local x,y = TheSim:GetWindowSize()
    local new_widget
    for i=1,#POS_INDEX do
        new_widget = widget:AddChild(UIAnim())
        local new_widget_AnimState = new_widget:GetAnimState()
        widget["leavesTop"..row.."_"..i] = new_widget
        new_widget:SetClickable(false)
        new_widget:SetHAnchor(ANCHOR_MIDDLE)
        new_widget:SetVAnchor(ANCHOR_TOP)
        new_widget_AnimState:SetDefaultEffectHandle("shaders/ui_anim_cc.ksh")
        new_widget_AnimState:SetBank("evil_canopy") --- this is my evil_canopy.zip bank
        new_widget_AnimState:SetBuild("evil_canopy") --- this is my evil_canopy.zip build
        new_widget:SetScaleMode(SCALEMODE_PROPORTIONAL)
        new_widget_AnimState:UseColourCube(true)
        new_widget_AnimState:SetUILightParams(2.0, 4.0, 4.0, 20.0)
        new_widget_AnimState:AnimateWhilePaused(false)
        new_widget:Hide()
        new_widget.x_offset = getxoffset()
        new_widget:SetPosition( new_widget.x_offset + POS_INDEX[i]*x/8,0 )
        new_widget.depth = ((row-1)*2)/ROWS

        local anim = math.random(ANIM_IDLES)
        if ROW_ANIMS[row-1] and ROW_ANIMS[row-1][i] then
            while anim == ROW_ANIMS[row-1][i] do
                anim = math.random(ANIM_IDLES)
            end
        end

        new_widget.animnum = anim
        new_widget_AnimState:PlayAnimation("idle"..new_widget.animnum,true)
        new_widget_AnimState:SetTime(math.random()*2)
        new_widget.anim_row = row
        new_widget.anim_pos = i

        new_widget_AnimState:SetMultColour(1, 1, 1, 1)

        ROW_ANIMS[row][i] = new_widget.animnum

        local scale = (math.random() < 0.5 and -1) or 1
        new_widget:SetScale(scale, 0, 1)

        new_widget_AnimState:Hide("flower01")
        new_widget_AnimState:Hide("flower02")
        new_widget_AnimState:Hide("flower03")
        new_widget_AnimState:Hide("flower04")
        new_widget_AnimState:Hide("flower05")
    end
end

local Evilcanopy = Class(Widget, function(self, owner)
    self.owner = owner
    Widget._ctor(self, "Evilcanopy")

    for i=1,ROWS do
        addcanopyrow(self,i)
    end

    self.leavestop_intensity = 0

    self.leavespercent = 0
end)

local function calcdepthmod(depth)
    depth = depth -1

    local mod = 0.5 * math.sqrt(1 - (depth*depth))
    return mod * -450
end

local function sortdepth(widget, mod)
    widget.depth = widget.depth - mod

    if widget.depth > 2 then
        widget.depth = widget.depth -2
        widget:MoveToBack()
        widget:GetAnimState():PlayAnimation("idle"..math.random(1,4),true)
        widget:GetAnimState():SetTime(math.random()*2)

        local x = widget:GetPosition().x - widget.x_offset
        local modx = getxoffset()
        widget.x_offset = modx
        widget:SetPosition(x + widget.x_offset ,widget:GetPosition().y)

    elseif widget.depth < 0 then
        widget.depth = widget.depth +2
        widget:MoveToFront()
        widget:GetAnimState():PlayAnimation("idle"..math.random(1,4),true)
        widget:GetAnimState():SetTime(math.random()*2)

        local x = widget:GetPosition().x - widget.x_offset
        local modx = getxoffset()
        widget.x_offset = modx
        widget:SetPosition(x + widget.x_offset ,widget:GetPosition().y)
    end

    return widget
end

local function showleaves(widget)
    for set=1,ROWS do
        for i=1,#POS_INDEX do
            widget["leavesTop"..set.."_"..i]:Show()
        end
    end
end

local function hideleaves(widget)
    for set=1,ROWS do
        for i=1,#POS_INDEX do
            widget["leavesTop"..set.."_"..i]:Hide()
        end
    end
end

local Normal_1_0_1,  -- = Vector3(1,0,1):GetNormalized()
    Normal_n1_0_1,  -- = Vector3(-1,0,1):GetNormalized()
    Normal_1_0_n1,  -- = Vector3(1,0,-1):GetNormalized()
    Normal_n1_0_n1,  -- = Vector3(-1,0,-1):GetNormalized()
    Normal_0_0_1,  -- = Vector3(0, 0, 1)
    Normal_0_0_n1,  -- = Vector3(0, 0, -1)
    Normal_1_0_0,  -- = Vector3(1, 0, 0)
    Normal_n1_0_0  -- = Vector3(-1, 0, 0)

function Evilcanopy:OnUpdate(dt)
   if TheNet:IsServerPaused() then return end

    local zoomoffset = 0
    if TheCamera.distance and not TheCamera.dollyzoom then
        zoomoffset = Remap(TheCamera.distance,30,45,0,-75)
        if TheCamera.distance < 30 then
            zoomoffset = zoomoffset *1.6
        end
    end

    local current_camera_x, current_camera_y, current_camera_z = TheCamera.currentpos:Get()
    for set=1,ROWS do
        for i=1,#POS_INDEX do
            self["leavesTop"..set.."_"..i]:GetAnimState():SetWorldSpaceAmbientLightPos(current_camera_x, current_camera_y, current_camera_z)
        end
    end

    self.under_leaves = self.owner._underevilcanopy and self.owner._underevilcanopy:value() --- important. This is what we are going to grab in our custom event. This is easy to miss so make sure to rename these.

    local SEC = 2
    self.leavestop_intensity = (self.under_leaves and math.min(1, self.leavestop_intensity+(1/(30 * SEC)) ))
        or math.max(0, self.leavestop_intensity-(1/(30 * SEC)) )

    if self.leavestop_intensity == 0 then
        hideleaves(self)
    else
        showleaves(self)

        local ypos = ((1-self.leavestop_intensity) *500) + zoomoffset  +300

        local thisframecoords = Vector3(current_camera_x, current_camera_y, current_camera_z)
        local down = TheCamera:GetDownVec()
        local down_x, down_z = down.x, down.z
        local diffcoords = (self.lastframecoords ~= nil and (thisframecoords - self.lastframecoords))
            or Vector3(0,0,0)

        local modx = 0
        local mody = 0

        --(0.71, 0.71)
        if down_x < 0.8 and down_x > 0.6 and down_z < 0.8 and down_z > 0.6 then
            Normal_n1_0_1 = Normal_n1_0_1 or Vector3(-1, 0, 1):GetNormalized()
            modx = diffcoords:Dot(Normal_n1_0_1)

            Normal_1_0_1 = Normal_1_0_1 or Vector3(1, 0, 1):GetNormalized()
            mody = diffcoords:Dot(Normal_1_0_1)
        end

        --(1.00, 0.00)
        if down_x > 0.8 and down_z < 0.1 and down_z > -0.1 then
            Normal_0_0_1 = Normal_0_0_1 or Vector3(0, 0, 1)
            modx = diffcoords:Dot(Normal_0_0_1)

            Normal_1_0_0 = Normal_1_0_0 or Vector3(1, 0, 0)
            mody = diffcoords:Dot(Normal_1_0_0)
        end

        --(0.71,-0.71)
        if down_x < 0.8 and down_x > 0.6 and down_z > -0.8 and down_z < -0.6 then
            Normal_1_0_1 = Normal_1_0_1 or Vector3(1, 0, 1):GetNormalized()
            modx = diffcoords:Dot(Normal_1_0_1)

            Normal_1_0_n1 = Normal_1_0_n1 or Vector3(1, 0, -1):GetNormalized()
            mody = diffcoords:Dot(Normal_1_0_n1)
        end

        --(0.0,-1)
        if down_x < 0.1 and down_x > -0.1 and down_z < -0.8 then
            Normal_1_0_0 = Normal_1_0_0 or Vector3(1, 0, 0)
            modx = diffcoords:Dot(Normal_1_0_0)

            Normal_0_0_n1 = Normal_0_0_n1 or Vector3(0, 0, -1)
            mody = diffcoords:Dot(Normal_0_0_n1)
        end

        --(-0.71, -0.71)
        if down_x > -0.8 and down_x < -0.6 and down_z > -0.8 and down_z < -0.6 then
            Normal_1_0_n1 = Normal_1_0_n1 or Vector3(1, 0, -1):GetNormalized()
            modx = diffcoords:Dot(Normal_1_0_n1)

            Normal_n1_0_n1 = Normal_n1_0_n1 or Vector3(-1, 0, -1):GetNormalized()
            mody = diffcoords:Dot(Normal_n1_0_n1)
        end

        --(-1.00, 0.00)
        if down_x < -0.8 and down_z < 0.1 and down_z > -0.1 then
            Normal_0_0_n1 = Normal_0_0_n1 or Vector3(0, 0, -1)
            modx = diffcoords:Dot(Normal_0_0_n1)

            Normal_n1_0_0 = Normal_n1_0_0 or Vector3(-1, 0, 0)
            mody = diffcoords:Dot(Normal_n1_0_0)
        end

        --(-0.71, 0.71)
        if down_x > -0.8 and down_x < -0.6 and down_z < 0.8 and down_z > 0.6 then
            Normal_n1_0_n1 = Normal_n1_0_n1 or Vector3(-1, 0, -1):GetNormalized()
            modx = diffcoords:Dot(Normal_n1_0_n1)

            Normal_n1_0_1 = Normal_n1_0_1 or Vector3(-1, 0, 1):GetNormalized()
            mody = diffcoords:Dot(Normal_n1_0_1)
        end

        --(0.00, 1.00)
        if down_x < 0.1 and down_x > -0.1 and down_z > 0.9 then
            Normal_n1_0_0 = Normal_n1_0_0 or Vector3(-1, 0, 0)
            modx = diffcoords:Dot(Normal_n1_0_0)

            Normal_0_0_1 = Normal_0_0_1 or Vector3(0, 0, 1)
            mody = diffcoords:Dot(Normal_0_0_1)
        end

        modx = modx * 100
        mody = mody * 0.2

        for set=1,ROWS do
            local depthmod = calcdepthmod(self["leavesTop"..set.."_1"].depth)
            for i=1,#POS_INDEX do
                local widget = sortdepth(self["leavesTop"..set.."_"..i], mody)
                self["leavesTop"..set.."_"..i] = widget

                local widget_position = widget:GetPosition()
                widget:SetPosition(widget_position.x-modx, ypos + depthmod)
                widget_position = widget:GetPosition()

                local sx, _ = TheSim:GetWindowSize()
                if widget_position.x > 5*sx/8 then
                    local modx_offset = getxoffset()
                    local adjust = modx_offset - widget.x_offset
                    widget.x_offset = modx_offset

                    widget:SetPosition(adjust + widget_position.x - (sx*5/4), widget_position.y)
                    setnewanim(widget)
                end
                if widget_position.x < -5*sx/8 then
                    local modx_offset = getxoffset()
                    local adjust = modx_offset - widget.x_offset
                    widget.x_offset = modx_offset

                    widget:SetPosition(adjust + widget_position.x + (sx*5/4), widget_position.y)
                    setnewanim(widget)
                end
            end
        end

        if self.leavesfullyin then
            self.leavespercent = self.leavespercent + mody
            if self.leavespercent > 1 then
                self.leavespercent = self.leavespercent -1
            end
            if self.leavespercent < 0 then
                self.leavespercent = self.leavespercent +1
            end
        else
            self.leavesfullyin = true
        end
        self.lastframecoords = thisframecoords
    end
end

return Evilcanopy

Next we return to our postini and create a new folder called prefabs. Inside we make a new script called wilson.lua.

We are going to patch in a new event here. Change all names to yours.

local ENV = env
GLOBAL.setfenv(1, GLOBAL)

local function OnChangeCanopyZone(inst, underleaves)
	inst._underevilcanopy:set(underleaves) --- this is the line I commented about in our new widget script
end

ENV.AddPlayerPostInit(function(inst)
	inst._underevilcanopy = net_bool(inst.GUID, "localplayer._underevilcanopy","underevilcanopydirty")

	if not TheWorld.ismastersim then
		return
	end
	inst:ListenForEvent("onchangeevilzone", OnChangeCanopyZone) --- this is the custom event you saw being called inside our pillar prefab.
end)

We're not done yet because we now have make sure it actually overlays on the screen. Go into our init folder and create a init_widgets.lua script and add this. Replace every evil instance with your name. 

local ENV = env
GLOBAL.setfenv(1, GLOBAL)

local Evilcanopy = require "widgets/evilcanopy"

ENV.AddClassPostConstruct("screens/playerhud", function(self)

	local OldCreateOverlays = self.CreateOverlays

	function self:CreateOverlays(owner, ...)
        OldCreateOverlays (self, owner, ...)
		self.evilcanopy = self.overlayroot:AddChild(Evilcanopy(owner)) 
	end

	local OldOnUpdate = self.OnUpdate

	function self:OnUpdate(dt, ...)
        OldOnUpdate (self, dt, ...)
		if self.evilcanopy then
			self.evilcanopy:OnUpdate(dt)
		end
	end

end)

 

Now that this is done, it's time to make our modmain notice our new canopy. Add  all these in, and if you've set up your modmain like me. It should look like this. 

require("evilforest_util")
require("evilforest_strings")

local inits = {
	"init_assets",
    "init_prefabs", --- I put my prefabs inside init, where my tree is located.
	"init_widgets",
}

for _, v in pairs(inits) do
	modimport("init/"..v)
end


local components = {
	"ambientsound",
}

for _, v in pairs(components) do
	modimport("postinit/components/"..v)
end

local prefabs = {
	"wilson",
}

for _, v in pairs(prefabs) do
	modimport("postinit/prefabs/"..v)
end

Load up your game and your pillars should be casting shadows and displaying your new leaves. 

image.png.fbcb12b7c317f4c46a409aa3bb2400b2.png

Making Setpieces with TILED

Spoiler

First make sure you have the latest Tiled downloaded. We will not be using the severely out of date version packaged with klei's mod tools. I will provide some DST images to make up your tilesets. These tilesets are mostly a visual representation for you, and do not matter much in the grand scheme of things because we are going to be setting our own tile IDs.

Please download them here  DST Tileset.zip

Once you have them downloaded, put the tileset images in a easy to find directory of your choosing. It doesn't matter where. 

Open up Tiled by creating a new project. I named mine eviltrees. Then click on New map which should open up the entire interface you will be working with. I chose a tile height and width of 3x3 because I'm going to be making my trees a bunch of tilesets. Making the trees tilesets prevents the trees from overlapping, and prevents ponds and other prefabs from spawning under them and prevents an unsightly appearance. It also prevents mobs and players accidently getting stuck on the collision because close spawning prefabs. 

So with this in mind, your interface should look like this.

image.png.28989b1fccd856b0dae4a5aa78c9a5a5.png

But oh no, we don't have a tileset, so let's import the tileset you just downloaded!

To do this, go to the Map tab, and click Add External Tileset.

Navigate to where you put your downloaded tileset and open the Don't Starve Together Terrains.tsx file located within the folder.

image.png.7248aee1de2130a8435b676dc440671a.png

It should now look like this. Make sure your tile width and height is at 64,otherwise your tile will be too big when you're trying to apply them. You can check and adjust by going to the Map tab and clicking on Map properties for the window to pop up.

Next you need to click on the Layers tab which is shown above your tilesets. Rename this to BG_TILES and create a new object layer named FG_OBJECTS. You can make a new layer by pressing that star/paper icon above the Mini-map tab.

It should look something like this now.

image.png.e8f5c951105162b7fb8e6f8ed7a4fae5.png

With the bg tile layer selected you can start picking from your tileset palette and paint out your floor textures. If you do not wish to have a set piece with a floor, you leave the grid empty. Normally I would leave my floor empty because all I'm doing is padding my evil tall trees. But for this tutorial I will paint my entire grid with several types of tiles.

Remember the tiles placed are just visual representations. So I can slap ocean textures on it if I wanted to, and all that would matter is it's ID which can and will be changed in code.

image.png.dc1dc777e44721743206e509e18f9781.png

Now that we have our beautiful floor down,  it's time to add our prefabs we want in our setpieces. In my case I'm just going to add a singular evil tall tree in the center. But you can put any and as many prefabs in there as you like. Your grid doesn't even need to be 6x6, it could be a entire island with over 120x120 in size if you want it to be.

So now click on your FG_OBJECTS layer. This will allow you to be able to use the tools on the top. In my case I like using the square tool. 

Once drawn onto the map, next click on the Objects tab next to Layers. This tab will allow you to see your object names and renamed them appropriately.

image.png.eeaafbd1fa1d1276deb6d18e7e34f516.png

Alright so this is where the new stuff comes in for the up to date tiled. Right now your prefab is an empty object that must be named. So on the left where its properties are. The Name is meaningless, you can name it anything, it's really just for organization sake especially if you have a setpiece with a ton of objects in it. The Class is important. This is the name of your game prefab.

In my case, my tall trees prefab is named eviltree_tall. Mine looks like this. 

image.png.9cdc55ac71f709716829124f50cf880d.png

Once finished, you now go to file and Export as Lua. Mines going to be named eviltrees.lua. Save it in a newly created static_layouts folder in your map folder in your mods scripts.

image.png.3429a903427a5320c7713bf2e7e5e81d.png

And that's it. Next we'll be implementing our new static layout setpiece and making them recognize our new tiles! 

Adding your setpiece to your biome

Spoiler

In your modworldgenmain.lua you are going to add this line of code, so it looks like this

local ENV = env
GLOBAL.setfenv(1, GLOBAL)


local modimport = ENV.modimport
--local GetModConfigData = ENV.GetModConfigData

modimport("init/init_tiles")

--	Setpieces

local Layouts = require("map/layouts").Layouts -- we must require both layouts and static_layouts 
local StaticLayout = require("map/static_layout")

local evil_layouts = {
	"eviltrees", --- this is our setpiece we just made. Mines as you previously saw was named eviltreees.lua. In this table you can add more.
}

for _, layout in ipairs(evil_layouts) do
	Layouts[layout] = StaticLayout.Get("map/static_layouts/"..string.lower(layout)) --- this is just recognizing our set piece's existence
	Layouts[layout].ground_types = EVIL_GROUND_TYPES --- This is what will tell the game to recognize our tiles. Replace any instance relating to evil with your own.
end

modimport("init/init_worldgen")

Our mod does not know what a EVIL_GROUND_TYPES is, so we're going to go into our init folder and open up our init_tiles script again and add them in. At the very bottom of your tiles.lua, add this.

--- rename this ground type to the one in your modworldgenmain. Make sure to keep note of those numbers. Thse are number IDs we are assigning these tiles in order. In our case I labeled my evilforest tile as number 2. We'll also grab number 5 which is forest tile. But you can add, and replace these tiles with new or custom ones easily if you which to change them.
GLOBAL.EVIL_GROUND_TYPES = {
	WORLD_TILES.IMPASSABLE, WORLD_TILES.EVILFOREST, WORLD_TILES.MUD, WORLD_TILES.GRASS, WORLD_TILES.FOREST, -- 1, 2, 3, 4, 5
	WORLD_TILES.OCEAN_EVIL, WORLD_TILES.DIRT, WORLD_TILES.ROCKY, WORLD_TILES.UNDERROCK, WORLD_TILES.MONKEY_DOCK, -- 6, 7, 8, 9, 10
	WORLD_TILES.OCEAN_COASTAL_SHORE, -- 11
}

Now that we're done with this, we now must go into our static_layouts folder and open our newly created setpiece. In my case, it's eviltrees.lua.

This is what my eviltrees.lua looks like.

version = "1.10",
  luaversion = "5.1",
  tiledversion = "1.10.2",
  class = "",
  orientation = "orthogonal",
  renderorder = "right-down",
  width = 3,
  height = 3,
  tilewidth = 64,
  tileheight = 64,
  nextlayerid = 3,
  nextobjectid = 2,
  properties = {},
  tilesets = {
    {
      name = "Don't Starve Together Terrains",
      firstgid = 1,
      filename = "../../../../../../Don't Starve Mod Tools/mod_tools/Tiled/DST Tileset/Don't Starve Together Terrains.tsx"
    }
  },
  layers = {
    {
      type = "tilelayer",
      x = 0,
      y = 0,
      width = 3,
      height = 3,
      id = 1,
      name = "BG_TILES",
      class = "",
      visible = true,
      opacity = 1,
      offsetx = 0,
      offsety = 0,
      parallaxx = 1,
      parallaxy = 1,
      properties = {},
      encoding = "lua",
      data = { --- this is the 3x3 map I made. You can see that the numbers are all wrong ang random. Well we're going to change these.
        160, 160, 154,
        160, 160, 160,
        154, 154, 160
      }
    },
    {
      type = "objectgroup",
      draworder = "topdown",
      id = 2,
      name = "FG_OBJECTS",
      class = "",
      visible = true,
      opacity = 1,
      offsetx = 0,
      offsety = 0,
      parallaxx = 1,
      parallaxy = 1,
      properties = {},
      objects = {
        {
          id = 1,
          name = "Eviltree",
          type = "eviltree_tall",
          shape = "rectangle",
          x = 61.3333,
          y = 57.3333,
          width = 68,
          height = 69.3333,
          rotation = 0,
          visible = true,
          properties = {}
        }
      }
    }
  }
}

In the data table we have a bunch of random numbers. Naturally you can change these in Tiled to save a few seconds but it's not really worth the hassle. We're going to change every 160 with 2 which is our custom turf, and 154 into a 5 which is forest turf.

Your data table should look like this afterwards.

      data = {
        2, 2, 5,
        2, 2, 2,
        5, 5, 2
      }

It's time to add our set pieces into the world generation. Let's stick it into our biome by adding it into our rooms! Mine's again called evilforest_rooms.lua, located in the mods scripts/map/rooms folder. If you remember, I left inside a countstaticlayouts section where I put a "skeleton_camper" inside. This line is for adding set pieces. I am going to replace it with my eviltrees setpiece.

AddRoom("BG EvilForest",  {
		colour={r=0.3,g=0.2,b=0.1,a=0.3},
		value = WORLD_TILES.EVILFOREST_NOISE,
		contents = {
			distributepercent = 0.07,
			distributeprefabs =
			{
				marsh_tree = 1,
				marsh_bush = 0.66,

			},
		}
	})

	AddRoom("EvilForest",  {
		colour={r=0.3,g=0.2,b=0.1,a=0.3},
		value = WORLD_TILES.EVILFOREST_NOISE,
		contents = {
			countstaticlayouts={["eviltrees"] = 10 + math.random(10, 18),}, --- this means 10 will always spawn plus 10 to 18 more trees will spawn. So thhis entire biome can have up to 28 evil tall trees in it.
			countprefabs= {
			burntground_faded = function() return math.random(3,5) end,
			},
			distributepercent = 0.07,
			distributeprefabs =
			{
				marsh_tree = 1,
				marsh_bush = 0.66,
				fireflies = 0.33,
				evergreen_sparse = 6,
			},
		},
		
	})
	
	
	AddRoom("EvilForest Ponds",  {
		colour={r=0.3,g=0.2,b=0.1,a=0.3},
		value = WORLD_TILES.EVILFOREST,
		contents = {			
			distributepercent = 0.1,
			distributeprefabs =
			{				
				pond_cave = 0.1,

				butterfly = .5,				
				flower = .3,
				evergreen_sparse = 3,
				notthebees = {weight = .2, prefabs = {"beehive", "wasphive"}},				
				grass = 1,
				marsh_tree = 1,
				fireflies = 0.33,
				marsh_bush = 0.66,			
			},
		},
		
	})
	

Now that everything's set up, let's check in game to see how our setpiece looks.

image.png.180df1f1a707cbac07186c57463854a6.png

And here we go! Nice, none overlapping trees. You can notice even the patches of grass in the mix too!

 Hope this helps everyone who has ever  wished to create their own biomes. If you have any questions let me know ^^

I also give thanks to @ADM for teaching me how to create biomes. So show him some love, as I would had never been able to create this tutorial without his help when I was making my own silly biomes last year.

Edited by Feything
  • Like 4
  • Happy Hazard 1
  • Sanity 1

It would be very interesting, if one could make something similar to the „maxwellhome“ with this (not a copy and no camera perspective thing, just similar in THEME but of random nature („maxwellhome chess biome“) and with DST only items, which I assume would be possible)

Can this also be done without any custom objects? (probably most silly question ever, sorry, but I feel uncertain)

2 minutes ago, NPCMaxwell said:

It would be very interesting, if one could make something similar to the „maxwellhome“ with this (not a copy and no camera perspective thing, just similar in THEME but of random nature („maxwellhome chess biome“) and with DST only items, which I assume would be possible)

Can this also be done without any custom objects? (probably most silly question ever, sorry, but I feel uncertain)

Yes, you can make biomes without custom objects. You just skip the tile texture creation steps, and fill out everything using existing prefabs, tiles, and textures in the game.

  • Health 1

Added Ambientsound section, so your turfs should not be awkwardly silent when you walk on them. Next I'll make some steps for diggable turfs, canopies I'll draw up some very tall trees and show how to make none-buggy tall prefabs and attach shade to them, and last the setpieces. If anyone wishes to know how to do more world generation stuff beyond what is listed, feel free to ask and I'll try my best to add to the guide.

A heads up, my keyboard is messed up, so if my guide is filled with typos it's because my keys are sticking, and my "W" key is broken.

Edited by Feything

Custom canopy section is finished. This section was difficult to put together, code heavy and I hope I did not miss anything. Adding the leafy covering over the biome was a completely new process so getting it to be customizable with new art was a process. But it is here now! 

  • Like 1
  • Thanks 1

Alright everyone, the tutorial for setpieces and basic biome creation is finished! I'll be editing the tutorial on and off to add some more tidbits but every major detail is hopefully accounted for at least on overworld land based biome creation.

  • Thanks 1

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
  • Create New...