Last Update: 2023-07-06
Creating a Complex Item with Multiple Components
This tutorial demonstrates how to create a more sophisticated item that uses multiple components to achieve complex behaviors. We'll create a "Soul Lantern" - a magical lantern that harvests souls from defeated enemies to power itself.
Project Overview
Our Soul Lantern will have these features:
- Provides light when equipped
- Harvests "souls" from nearby defeated enemies
- Stores souls as fuel
- Provides sanity aura based on stored souls
- Changes appearance based on fuel level
- Can be recharged by defeating enemies
Components We'll Use
This item will use several components working together:
equippable
- To allow the item to be equippedinventoryitem
- To allow the item to be picked up and storedfueled
- To manage the soul energy levelsanityaura
- To provide sanity benefits based on fuel levellightsource
- To provide illuminationweapon
- To allow combat usagefiniteuses
- To manage durability when used as a weaponinspectable
- To provide examination text
Step 1: Set Up the Mod Structure
Create these folders and files:
soul_lantern_mod/
├── modinfo.lua
├── modmain.lua
├── scripts/
│ └── prefabs/
│ └── soul_lantern.lua
└── images/
└── inventoryimages/
├── soul_lantern.png
└── soul_lantern.xml
Step 2: Create the modinfo.lua File
name = "Soul Lantern"
description = "A lantern that harvests souls from the fallen to power itself."
author = "Your Name"
version = "1.0.0"
-- Compatible with Don't Starve Together
dst_compatible = true
dont_starve_compatible = false
-- This mod is required on clients
all_clients_require_mod = true
client_only_mod = false
-- Icon displayed in the server list
icon_atlas = "modicon.xml"
icon = "modicon.tex"
-- Tags that describe your mod
server_filter_tags = {
"item",
"magic"
}
-- Configuration options
configuration_options = {
{
name = "soul_value",
label = "Soul Energy Value",
options = {
{description = "Low", data = 10},
{description = "Medium", data = 20},
{description = "High", data = 30}
},
default = 20
},
{
name = "max_fuel",
label = "Maximum Soul Capacity",
options = {
{description = "Small", data = 100},
{description = "Medium", data = 200},
{description = "Large", data = 300}
},
default = 200
},
{
name = "weapon_damage",
label = "Weapon Damage",
options = {
{description = "Low", data = 17},
{description = "Medium", data = 27},
{description = "High", data = 37}
},
default = 27
}
}
Step 3: Create the modmain.lua File
-- Import globals into the environment
GLOBAL.setmetatable(env, {__index = function(t, k) return GLOBAL.rawget(GLOBAL, k) end})
-- Add asset files
Assets = {
-- Inventory image
Asset("IMAGE", "images/inventoryimages/soul_lantern.tex"),
Asset("ATLAS", "images/inventoryimages/soul_lantern.xml"),
}
-- Register the prefab
PrefabFiles = {
"soul_lantern",
}
-- Add recipe to craft the soul lantern
AddRecipe("soul_lantern",
{
Ingredient("boards", 2),
Ingredient("nightmarefuel", 3),
Ingredient("gears", 1)
},
GLOBAL.RECIPETABS.MAGIC,
GLOBAL.TECH.MAGIC_TWO,
nil, nil, nil, nil, nil,
"images/inventoryimages/soul_lantern.xml", "soul_lantern.tex")
-- Add custom strings
GLOBAL.STRINGS.NAMES.SOUL_LANTERN = "Soul Lantern"
GLOBAL.STRINGS.CHARACTERS.GENERIC.DESCRIBE.SOUL_LANTERN = {
EMPTY = "It hungers for souls.",
LOW = "It contains a few captured souls.",
MEDIUM = "The souls inside are quite restless.",
FULL = "It's brimming with captured souls!"
}
GLOBAL.STRINGS.RECIPE_DESC.SOUL_LANTERN = "Harvest souls to light your way."
-- Listen for entity death to collect souls
AddPrefabPostInit("world", function(inst)
if GLOBAL.TheWorld.ismastersim then
inst:ListenForEvent("entity_death", function(world, data)
if data.inst and data.inst:HasTag("monster") then
-- Find nearby players with soul lanterns
local x, y, z = data.inst.Transform:GetWorldPosition()
local players = GLOBAL.TheSim:FindEntities(x, y, z, 20, {"player"})
for _, player in ipairs(players) do
if player.components.inventory then
-- Check equipped items
local equipped = player.components.inventory:GetEquippedItem(GLOBAL.EQUIPSLOTS.HANDS)
if equipped and equipped.prefab == "soul_lantern" then
-- Add fuel to the lantern
if equipped.components.fueled then
local soul_value = GetModConfigData("soul_value")
equipped.components.fueled:DoDelta(soul_value)
-- Spawn visual effect
local fx = GLOBAL.SpawnPrefab("statue_transition_2")
if fx then
fx.Transform:SetPosition(data.inst.Transform:GetWorldPosition())
fx.Transform:SetScale(0.5, 0.5, 0.5)
end
-- Play sound
equipped.SoundEmitter:PlaySound("dontstarve/common/nightmareAddFuel")
end
break
end
end
end
end
end)
end
end)
Step 4: Create the Prefab File
Create scripts/prefabs/soul_lantern.lua
:
local assets = {
Asset("ANIM", "anim/lantern.zip"),
Asset("ANIM", "anim/swap_lantern.zip"),
-- Inventory image
Asset("IMAGE", "images/inventoryimages/soul_lantern.tex"),
Asset("ATLAS", "images/inventoryimages/soul_lantern.xml"),
}
local prefabs = {
"lanternlight",
}
-- Light radius and intensity based on fuel level
local function GetLightRadius(inst)
local fueled = inst.components.fueled
if fueled then
local percent = fueled:GetPercent()
return Lerp(1.5, 3.5, percent)
end
return 2.0
end
local function GetLightIntensity(inst)
local fueled = inst.components.fueled
if fueled then
local percent = fueled:GetPercent()
return Lerp(0.4, 0.8, percent)
end
return 0.6
end
-- Update light color based on fuel level
local function UpdateLightColor(inst)
local fueled = inst.components.fueled
if fueled and inst.light then
local percent = fueled:GetPercent()
-- Shift from blue (low) to purple (full)
local r = Lerp(0.1, 0.6, percent)
local g = Lerp(0.1, 0.0, percent)
local b = Lerp(0.7, 0.8, percent)
inst.Light:SetColour(r, g, b)
-- Update light radius and intensity
inst.Light:SetRadius(GetLightRadius(inst))
inst.Light:SetIntensity(GetLightIntensity(inst))
end
end
-- Function to update the appearance based on fuel level
local function UpdateAppearance(inst)
if not inst.components.fueled then return end
local percent = inst.components.fueled:GetPercent()
-- Update animation based on fuel level
if percent <= 0.25 then
inst.AnimState:OverrideSymbol("lantern_overlay", "lantern", "lantern_overlay_low")
inst.components.inspectable.nameoverride = "SOUL_LANTERN_EMPTY"
elseif percent <= 0.5 then
inst.AnimState:OverrideSymbol("lantern_overlay", "lantern", "lantern_overlay_med")
inst.components.inspectable.nameoverride = "SOUL_LANTERN_LOW"
elseif percent <= 0.75 then
inst.AnimState:OverrideSymbol("lantern_overlay", "lantern", "lantern_overlay_high")
inst.components.inspectable.nameoverride = "SOUL_LANTERN_MEDIUM"
else
inst.AnimState:OverrideSymbol("lantern_overlay", "lantern", "lantern_overlay_full")
inst.components.inspectable.nameoverride = "SOUL_LANTERN_FULL"
end
-- Update sanity aura based on fuel level
if inst.components.equippable and inst.components.equippable:IsEquipped() then
local sanity_bonus = Lerp(0.1, 1.0, percent)
inst.components.equippable.dapperness = sanity_bonus / TUNING.TOTAL_DAY_TIME
end
-- Update light color
UpdateLightColor(inst)
end
-- Function called when the lantern is turned on
local function OnTurnOn(inst)
if inst.components.fueled then
inst.components.fueled:StartConsuming()
end
-- Create light
if inst.light == nil then
inst.light = SpawnPrefab("lanternlight")
inst.light.entity:SetParent(inst.entity)
end
-- Update light properties
UpdateLightColor(inst)
-- Update animation
inst.AnimState:PlayAnimation("idle_on")
-- Add minimap icon
inst.MiniMapEntity:SetIcon("soul_lantern.tex")
-- Add tag for light source
inst:AddTag("lightsource")
end
-- Function called when the lantern is turned off
local function OnTurnOff(inst)
if inst.components.fueled then
inst.components.fueled:StopConsuming()
end
-- Remove light
if inst.light ~= nil then
inst.light:Remove()
inst.light = nil
end
-- Update animation
inst.AnimState:PlayAnimation("idle_off")
-- Remove minimap icon
inst.MiniMapEntity:SetIcon(nil)
-- Remove light source tag
inst:RemoveTag("lightsource")
end
-- Function called when the lantern is equipped
local function OnEquip(inst, owner)
-- Use the regular lantern animation for now
owner.AnimState:OverrideSymbol("swap_object", "swap_lantern", "swap_lantern")
owner.AnimState:Show("ARM_carry")
owner.AnimState:Hide("ARM_normal")
-- Apply a purple tint to the lantern when equipped
owner.AnimState:SetMultColour(0.9, 0.8, 1, 1)
-- Turn on the lantern when equipped
if inst.components.fueled:GetPercent() > 0 then
OnTurnOn(inst)
end
-- Update sanity aura
local percent = inst.components.fueled:GetPercent()
local sanity_bonus = Lerp(0.1, 1.0, percent)
inst.components.equippable.dapperness = sanity_bonus / TUNING.TOTAL_DAY_TIME
end
-- Function called when the lantern is unequipped
local function OnUnequip(inst, owner)
owner.AnimState:Hide("ARM_carry")
owner.AnimState:Show("ARM_normal")
owner.AnimState:SetMultColour(1, 1, 1, 1)
-- Turn off the lantern when unequipped
OnTurnOff(inst)
-- Reset sanity aura
inst.components.equippable.dapperness = 0
end
-- Function called when attacking with the lantern
local function OnAttack(inst, owner, target)
if target and target:HasTag("ghost") then
-- Extra damage to ghosts
return inst.components.weapon.damage * 1.5
end
-- Chance to steal soul on hit
if target and target:HasTag("monster") and math.random() < 0.2 then -- 20% chance
if inst.components.fueled then
local soul_value = GetModConfigData("soul_value") / 2
inst.components.fueled:DoDelta(soul_value)
-- Visual effect
local fx = SpawnPrefab("statue_transition")
if fx then
fx.Transform:SetPosition(target.Transform:GetWorldPosition())
fx.Transform:SetScale(0.3, 0.3, 0.3)
end
end
end
end
-- Function called when fuel is depleted
local function OnOutOfFuel(inst)
if inst.components.equippable and inst.components.equippable:IsEquipped() then
OnTurnOff(inst)
end
-- Update appearance
UpdateAppearance(inst)
end
-- Function called when fuel level changes
local function OnFuelChanged(inst, data)
-- Update appearance based on new fuel level
UpdateAppearance(inst)
-- If equipped and turned off but now has fuel, turn it on
if inst.components.equippable and
inst.components.equippable:IsEquipped() and
not inst:HasTag("lightsource") and
inst.components.fueled:GetPercent() > 0 then
OnTurnOn(inst)
end
end
-- Main function to create the soul lantern
local function fn()
-- Create the entity
local inst = CreateEntity()
-- Add required components
inst.entity:AddTransform()
inst.entity:AddAnimState()
inst.entity:AddSoundEmitter()
inst.entity:AddMiniMapEntity()
inst.entity:AddNetwork()
-- Set up minimap icon
inst.MiniMapEntity:SetIcon("lantern.tex")
-- Set up physics
MakeInventoryPhysics(inst)
-- Set up animation
inst.AnimState:SetBank("lantern")
inst.AnimState:SetBuild("lantern")
inst.AnimState:PlayAnimation("idle_off")
-- Apply a purple tint to the lantern
inst.AnimState:SetMultColour(0.9, 0.8, 1, 1)
-- Add light component
inst.entity:AddLight()
inst.Light:SetFalloff(0.7)
inst.Light:SetIntensity(0.6)
inst.Light:SetRadius(2.0)
inst.Light:SetColour(0.4, 0.0, 0.8)
inst.Light:Enable(false)
-- Add tags
inst:AddTag("light")
inst:AddTag("soulharvester")
-- Make the entity pristine for networking
inst.entity:SetPristine()
if not TheWorld.ismastersim then
return inst
end
-- Add components
inst:AddComponent("inventoryitem")
inst.components.inventoryitem.imagename = "soul_lantern"
inst.components.inventoryitem.atlasname = "images/inventoryimages/soul_lantern.xml"
-- Add fueled component to manage soul energy
inst:AddComponent("fueled")
inst.components.fueled.fueltype = FUELTYPE.NIGHTMARE
inst.components.fueled:InitializeFuelLevel(0)
inst.components.fueled:SetDepletedFn(OnOutOfFuel)
inst.components.fueled:SetUpdateFn(UpdateLightColor)
inst.components.fueled:SetSections(4)
inst.components.fueled.accepting = true
inst.components.fueled:SetSectionCallback(UpdateAppearance)
inst.components.fueled.maxfuel = GetModConfigData("max_fuel")
inst.components.fueled.currentfuel = 0
-- Listen for fuel changes
inst:ListenForEvent("percentusedchange", OnFuelChanged)
-- Add weapon component
inst:AddComponent("weapon")
inst.components.weapon:SetDamage(GetModConfigData("weapon_damage"))
inst.components.weapon:SetOnAttack(OnAttack)
-- Add durability for weapon usage
inst:AddComponent("finiteuses")
inst.components.finiteuses:SetMaxUses(150)
inst.components.finiteuses:SetUses(150)
inst.components.finiteuses:SetOnFinished(function(inst) inst:Remove() end)
inst.components.finiteuses:SetConsumption(ACTIONS.ATTACK, 1)
-- Make it inspectable with dynamic descriptions
inst:AddComponent("inspectable")
inst.components.inspectable.nameoverride = "SOUL_LANTERN_EMPTY"
-- Make it equippable
inst:AddComponent("equippable")
inst.components.equippable:SetOnEquip(OnEquip)
inst.components.equippable:SetOnUnequip(OnUnequip)
-- Add a special effect when near ghosts
inst:DoPeriodicTask(1, function()
if inst.components.equippable and inst.components.equippable:IsEquipped() then
local owner = inst.components.inventoryitem.owner
if owner then
local x, y, z = owner.Transform:GetWorldPosition()
local ghosts = TheSim:FindEntities(x, y, z, 10, {"ghost"})
if #ghosts > 0 then
-- Make the light flicker when ghosts are nearby
if inst.light then
local intensity = GetLightIntensity(inst) * (0.8 + 0.4 * math.random())
inst.Light:SetIntensity(intensity)
-- Play a subtle sound occasionally
if math.random() < 0.1 then -- 10% chance each second
inst.SoundEmitter:PlaySound("dontstarve/common/haunted_1", nil, 0.3)
end
end
end
end
end
end)
return inst
end
-- Register the prefab
return Prefab("soul_lantern", fn, assets, prefabs)
Step 5: Component Interactions
Let's examine how the different components interact in our Soul Lantern:
Fueled + Equippable Interaction
The fueled
component manages the soul energy level, while the equippable
component handles when the item is worn. These interact in several ways:
- When equipped, the lantern checks fuel level and turns on if fuel is available
- When unequipped, the lantern turns off to conserve fuel
- The sanity aura (dapperness) of the equipped lantern scales with fuel level
-- When equipped, check fuel and turn on if possible
local function OnEquip(inst, owner)
-- ... other equip code ...
-- Turn on the lantern when equipped if it has fuel
if inst.components.fueled:GetPercent() > 0 then
OnTurnOn(inst)
end
-- Update sanity aura based on fuel level
local percent = inst.components.fueled:GetPercent()
local sanity_bonus = Lerp(0.1, 1.0, percent)
inst.components.equippable.dapperness = sanity_bonus / TUNING.TOTAL_DAY_TIME
end
Fueled + Light Interaction
The light properties (radius, intensity, color) change based on the fuel level:
-- Update light color based on fuel level
local function UpdateLightColor(inst)
local fueled = inst.components.fueled
if fueled and inst.light then
local percent = fueled:GetPercent()
-- Shift from blue (low) to purple (full)
local r = Lerp(0.1, 0.6, percent)
local g = Lerp(0.1, 0.0, percent)
local b = Lerp(0.7, 0.8, percent)
inst.Light:SetColour(r, g, b)
-- Update light radius and intensity
inst.Light:SetRadius(GetLightRadius(inst))
inst.Light:SetIntensity(GetLightIntensity(inst))
end
end
Weapon + Fueled Interaction
Using the lantern as a weapon can generate additional fuel when hitting monsters:
-- Function called when attacking with the lantern
local function OnAttack(inst, owner, target)
-- ... other attack code ...
-- Chance to steal soul on hit
if target and target:HasTag("monster") and math.random() < 0.2 then -- 20% chance
if inst.components.fueled then
local soul_value = GetModConfigData("soul_value") / 2
inst.components.fueled:DoDelta(soul_value)
-- Visual effect
local fx = SpawnPrefab("statue_transition")
if fx then
fx.Transform:SetPosition(target.Transform:GetWorldPosition())
fx.Transform:SetScale(0.3, 0.3, 0.3)
end
end
end
end
Inspectable + Fueled Interaction
The inspection text changes based on the fuel level:
-- Function to update the appearance based on fuel level
local function UpdateAppearance(inst)
if not inst.components.fueled then return end
local percent = inst.components.fueled:GetPercent()
-- Update inspection text based on fuel level
if percent <= 0.25 then
inst.components.inspectable.nameoverride = "SOUL_LANTERN_EMPTY"
elseif percent <= 0.5 then
inst.components.inspectable.nameoverride = "SOUL_LANTERN_LOW"
elseif percent <= 0.75 then
inst.components.inspectable.nameoverride = "SOUL_LANTERN_MEDIUM"
else
inst.components.inspectable.nameoverride = "SOUL_LANTERN_FULL"
end
-- ... other appearance updates ...
}
Step 6: Testing Your Mod
- Launch Don't Starve Together
- Enable your mod in the Mods menu
- Start a new game
- Craft your Soul Lantern (requires boards, nightmare fuel, and gears)
- Test the lantern by:
- Equipping it to see the light effect
- Killing monsters to collect souls
- Watching the appearance change as it fills with souls
- Using it as a weapon and observing the special effects
- Checking the sanity boost as the lantern fills
Customization Options
Here are some ways to enhance your Soul Lantern:
Add Custom Animations
For a fully custom appearance, create your own animation files:
- Create
anim/soul_lantern.zip
with custom animations - Update the prefab file:
local assets = {
Asset("ANIM", "anim/soul_lantern.zip"),
Asset("ANIM", "anim/swap_soul_lantern.zip"),
-- Other assets...
}
-- Then in the fn() function:
inst.AnimState:SetBank("soul_lantern")
inst.AnimState:SetBuild("soul_lantern")
Add More Soul Collection Methods
Expand the ways to collect souls:
-- In modmain.lua, add this to collect souls from ghosts being caught
AddPrefabPostInit("ghostlyelixir_slowregen", function(inst)
if inst.components.ghostlyelixir then
local old_apply = inst.components.ghostlyelixir.onconsumedfn
inst.components.ghostlyelixir.onconsumedfn = function(inst, ghost, doer)
if old_apply then
old_apply(inst, ghost, doer)
end
-- Check if the catcher has a soul lantern
if doer and doer.components.inventory then
local lantern = doer.components.inventory:FindItem(function(item)
return item.prefab == "soul_lantern"
end)
if lantern and lantern.components.fueled then
-- Add extra souls when catching a ghost
lantern.components.fueled:DoDelta(GetModConfigData("soul_value") * 2)
-- Visual effect
local fx = SpawnPrefab("statue_transition_2")
if fx then
fx.Transform:SetPosition(ghost.Transform:GetWorldPosition())
end
end
end
end
end
end)
Add Special Powers Based on Fuel Level
Give the lantern additional abilities when fully charged:
-- In the OnEquip function, add:
inst:DoPeriodicTask(1, function()
if inst.components.fueled:GetPercent() > 0.9 then -- When nearly full
local owner = inst.components.inventoryitem.owner
if owner then
-- Create a protective aura
local x, y, z = owner.Transform:GetWorldPosition()
local ents = TheSim:FindEntities(x, y, z, 5, {"ghost", "monster"})
for _, ent in ipairs(ents) do
if ent.components.combat and ent.components.combat:TargetIs(owner) then
-- Chance to make monsters flee
if math.random() < 0.1 then -- 10% chance each second
if ent.components.combat then
ent.components.combat:DropTarget()
end
if ent.components.locomotor then
ent.components.locomotor:RunAway(owner, 10, 10)
end
-- Visual effect
local fx = SpawnPrefab("statue_transition")
if fx then
fx.Transform:SetPosition(ent.Transform:GetWorldPosition())
end
end
end
end
end
end
end)
Common Issues and Solutions
Problem: Components not interacting correctly
Solution: Make sure you're updating component properties in the right order and checking if components exist before using them
Problem: Fuel not being consumed or added correctly
Solution: Verify that you're using the correct fuel type and that the fueled component is properly initialized
Problem: Visual effects not appearing
Solution: Check that you're spawning the effects at the correct position and that the prefabs exist in the game
Problem: Sanity aura not working
Solution: Make sure you're setting the dapperness value correctly and updating it when fuel changes
Advanced Component Integration
For even more complex items, consider these advanced techniques:
Creating Custom Components
If existing components don't provide the functionality you need, you can create custom components:
-- In scripts/components/soulcollector.lua
local SoulCollector = Class(function(self, inst)
self.inst = inst
self.souls_collected = 0
self.max_souls = 100
self.soul_power = 1
self.on_collect_fn = nil
end)
function SoulCollector:CollectSoul(value)
value = value or self.soul_power
self.souls_collected = math.min(self.souls_collected + value, self.max_souls)
if self.on_collect_fn then
self.on_collect_fn(self.inst, self.souls_collected)
end
return true
end
function SoulCollector:SetOnCollectFn(fn)
self.on_collect_fn = fn
end
return SoulCollector
Component Communication Through Events
Components can communicate through events for more complex interactions:
-- When fuel changes, trigger a custom event
inst:ListenForEvent("percentusedchange", function(inst, data)
inst:PushEvent("soulschanged", {percent = data.percent})
end)
-- Listen for the custom event in another component
inst:ListenForEvent("soulschanged", function(inst, data)
-- Update other components based on soul level
if inst.components.weapon then
local base_damage = GetModConfigData("weapon_damage")
local bonus = math.floor(base_damage * data.percent * 0.5)
inst.components.weapon:SetDamage(base_damage + bonus)
end
end)
Next Steps
Now that you've created a complex item with multiple interacting components, you can:
- Add More Component Interactions: Create even more complex behaviors through component communication
- Create a Set of Related Items: Design items that work together or share resources
- Add Character-Specific Behaviors: Make the item behave differently based on who's using it
- Expand with Custom Components: Create entirely new components for unique behaviors
For more advanced modding techniques, check out the Custom Component and Stategraph Mod tutorials.