Feything Posted March 21, 2025 Share Posted March 21, 2025 (edited) 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. TexTool: https://github.com/zxcvbnm3057/dont-starve-tools 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/ Tiled: https://www.mapeditor.org 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: 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. 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. 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. 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. 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. 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. 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. 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 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 ^^; 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. 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. 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. 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. 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. 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. Below, 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") 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. 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. 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. 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. 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. 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. 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. 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. 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. 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 February 19 by Feything 4 1 1 Link to comment https://forums.kleientertainment.com/forums/topic/164993-tutorial-custom-biome-creation/ Share on other sites More sharing options...
NPCMaxwell Posted March 21, 2025 Share Posted March 21, 2025 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) Link to comment https://forums.kleientertainment.com/forums/topic/164993-tutorial-custom-biome-creation/#findComment-1808524 Share on other sites More sharing options...
Feything Posted March 21, 2025 Author Share Posted March 21, 2025 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. 1 Link to comment https://forums.kleientertainment.com/forums/topic/164993-tutorial-custom-biome-creation/#findComment-1808526 Share on other sites More sharing options...
glooomy Posted March 21, 2025 Share Posted March 21, 2025 Amazing! TYSM for this! Link to comment https://forums.kleientertainment.com/forums/topic/164993-tutorial-custom-biome-creation/#findComment-1808527 Share on other sites More sharing options...
Feything Posted March 22, 2025 Author Share Posted March 22, 2025 (edited) 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 March 22, 2025 by Feything Link to comment https://forums.kleientertainment.com/forums/topic/164993-tutorial-custom-biome-creation/#findComment-1808553 Share on other sites More sharing options...
mathem99 Posted March 22, 2025 Share Posted March 22, 2025 Woah, amazing! Does this also work with Don't Starve (singleplayer)? Link to comment https://forums.kleientertainment.com/forums/topic/164993-tutorial-custom-biome-creation/#findComment-1808567 Share on other sites More sharing options...
Feything Posted March 22, 2025 Author Share Posted March 22, 2025 4 hours ago, mathem99 said: Woah, amazing! Does this also work with Don't Starve (singleplayer)? I have no idea, unfortunately ^^; Link to comment https://forums.kleientertainment.com/forums/topic/164993-tutorial-custom-biome-creation/#findComment-1808573 Share on other sites More sharing options...
Feything Posted March 24, 2025 Author Share Posted March 24, 2025 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! 1 1 Link to comment https://forums.kleientertainment.com/forums/topic/164993-tutorial-custom-biome-creation/#findComment-1808813 Share on other sites More sharing options...
Feything Posted April 3, 2025 Author Share Posted April 3, 2025 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. 1 Link to comment https://forums.kleientertainment.com/forums/topic/164993-tutorial-custom-biome-creation/#findComment-1810704 Share on other sites More sharing options...
Recommended Posts
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 accountSign in
Already have an account? Sign in here.
Sign In Now