Last Update: 2023-07-06
Custom Boss Project
This tutorial guides you through creating a complete mod that adds a custom boss to Don't Starve Together. We'll create the "Ancient Guardian," a powerful entity with unique behaviors, attacks, and drops.
Project Overview
By the end of this tutorial, you'll have created:
- A fully functional boss entity with custom AI
- Special attack patterns and abilities
- Custom animations and sound effects
- Unique loot drops
- Boss-specific game mechanics
Prerequisites
- Intermediate understanding of Lua programming
- Familiarity with Don't Starve Together modding
- Basic knowledge of state graphs and AI
- Understanding of prefabs and components
Project Structure
AncientGuardianMod/
├── modinfo.lua
├── modmain.lua
├── scripts/
│ ├── prefabs/
│ │ ├── ancient_guardian.lua
│ │ ├── ancient_guardian_horn.lua
│ │ └── ancient_essence.lua
│ ├── stategraphs/
│ │ └── SGancient_guardian.lua
│ └── brains/
│ └── ancient_guardian_brain.lua
└── anim/
└── ancient_guardian.zip
Step 1: Setting Up the Mod
First, let's create the basic mod structure and files:
modinfo.lua
name = "Ancient Guardian Boss"
description = "Adds a powerful Ancient Guardian boss with unique abilities and drops."
author = "Your Name"
version = "1.0.0"
-- Compatible with Don't Starve Together
dst_compatible = true
dont_starve_compatible = false
reign_of_giants_compatible = false
-- Tags to help users find the mod
all_clients_require_mod = true
client_only_mod = false
-- Icon and priority
icon_atlas = "modicon.xml"
icon = "modicon.tex"
-- Mod configuration options
configuration_options = {
{
name = "BOSS_HEALTH",
label = "Boss Health",
options = {
{description = "Easy (1000)", data = 1000},
{description = "Normal (2000)", data = 2000},
{description = "Hard (3000)", data = 3000},
{description = "Nightmare (5000)", data = 5000}
},
default = 2000
},
{
name = "BOSS_DAMAGE",
label = "Boss Damage",
options = {
{description = "Low", data = 0.75},
{description = "Normal", data = 1.0},
{description = "High", data = 1.5},
{description = "Extreme", data = 2.0}
},
default = 1.0
},
{
name = "SPAWN_MODE",
label = "Spawn Mode",
options = {
{description = "Ritual Only", data = "ritual"},
{description = "Natural Spawn", data = "natural"},
{description = "Both", data = "both"}
},
default = "both"
}
}
modmain.lua
-- Assets to preload
Assets = {
Asset("ATLAS", "images/inventoryimages/ancient_guardian_horn.xml"),
Asset("IMAGE", "images/inventoryimages/ancient_guardian_horn.tex"),
Asset("ATLAS", "images/inventoryimages/ancient_essence.xml"),
Asset("IMAGE", "images/inventoryimages/ancient_essence.tex"),
Asset("SOUND", "sound/ancient_guardian.fsb"),
}
-- Prefabs to register
PrefabFiles = {
"ancient_guardian",
"ancient_guardian_horn",
"ancient_essence",
}
-- Import configuration
local BOSS_HEALTH = GetModConfigData("BOSS_HEALTH")
local BOSS_DAMAGE = GetModConfigData("BOSS_DAMAGE")
local SPAWN_MODE = GetModConfigData("SPAWN_MODE")
-- Make configuration available to prefabs
TUNING.ANCIENT_GUARDIAN = {
HEALTH = BOSS_HEALTH,
DAMAGE_MULT = BOSS_DAMAGE,
SPAWN_MODE = SPAWN_MODE,
}
-- Add recipes for summoning items
local ancient_ritual = Recipe("ancient_ritual_item",
{Ingredient("goldnugget", 10), Ingredient("nightmarefuel", 5), Ingredient("purplegem", 1)},
RECIPETABS.MAGIC,
TECH.MAGIC_THREE)
ancient_ritual.atlas = "images/inventoryimages/ancient_ritual_item.xml"
-- Add a global function for spawning the boss
GLOBAL.SpawnAncientGuardian = function(pt)
if pt == nil then
-- If no position is provided, try to spawn near the player
local player = GLOBAL.ConsoleCommandPlayer()
if player then
pt = player:GetPosition()
-- Offset the position slightly
pt = pt + GLOBAL.Vector3(15, 0, 0)
else
return false
end
end
-- Spawn the boss
local guardian = GLOBAL.SpawnPrefab("ancient_guardian")
if guardian then
guardian.Transform:SetPosition(pt.x, pt.y, pt.z)
-- Spawn effects
GLOBAL.SpawnPrefab("statue_transition").Transform:SetPosition(pt.x, pt.y, pt.z)
GLOBAL.SpawnPrefab("statue_transition_2").Transform:SetPosition(pt.x, pt.y, pt.z)
return guardian
end
return false
end
-- Add boss to the world
AddPrefabPostInit("world", function(inst)
if SPAWN_MODE == "natural" or SPAWN_MODE == "both" then
-- Add the boss to the world generation
if inst.ismastersim then
inst:DoTaskInTime(5, function()
-- Spawn the boss in a suitable location after world generation
local function TrySpawnBoss()
local valid_spawns = {}
-- Find suitable spawn locations
for i, node in ipairs(GLOBAL.TheWorld.topology.nodes) do
if node.tags and
(table.contains(node.tags, "Rocky") or
table.contains(node.tags, "Cave")) then
table.insert(valid_spawns, node)
end
end
if #valid_spawns > 0 then
-- Choose a random valid location
local spawn_node = valid_spawns[math.random(#valid_spawns)]
local pos = GLOBAL.Vector3(spawn_node.x, 0, spawn_node.y)
-- Spawn the boss
GLOBAL.SpawnAncientGuardian(pos)
return true
end
return false
end
-- Try to spawn the boss
TrySpawnBoss()
end)
end
end
end)
## Step 2: Creating the Boss Prefab
Now let's create the main boss prefab:
### scripts/prefabs/ancient_guardian.lua
```lua
local assets = {
Asset("ANIM", "anim/ancient_guardian.zip"),
Asset("SOUND", "sound/ancient_guardian.fsb"),
}
local prefabs = {
"ancient_guardian_horn",
"ancient_essence",
"nightmarefuel",
"thulecite",
"thulecite_pieces",
"purplegem",
}
-- Import the brain and stategraph
local brain = require "brains/ancient_guardian_brain"
-- Special attack definitions
local SLAM_DAMAGE = 100
local CHARGE_DAMAGE = 75
local SWIPE_DAMAGE = 50
-- Sound effects
local sounds = {
idle = "ancientguardian/idle",
hurt = "ancientguardian/hurt",
death = "ancientguardian/death",
attack = "ancientguardian/attack",
charge_pre = "ancientguardian/charge_pre",
charge = "ancientguardian/charge",
slam = "ancientguardian/slam",
}
-- Function to handle when the boss takes damage
local function OnHit(inst, attacker, damage)
if inst.components.health:GetPercent() <= 0.5 and not inst.enraged then
-- Enter enraged state at 50% health
inst.enraged = true
inst.AnimState:SetMultColour(0.9, 0.3, 0.3, 1)
inst.components.combat:SetDefaultDamage(inst.base_damage * 1.5)
inst.components.locomotor:SetExternalSpeedMultiplier(inst, "enraged", 1.3)
-- Play enrage animation and sound
inst.AnimState:PlayAnimation("taunt")
inst.SoundEmitter:PlaySound(sounds.attack, "enrage")
-- Spawn nightmare fuel around the boss
local pos = inst:GetPosition()
for i = 1, 5 do
local offset = Vector3(math.random(-3, 3), 0, math.random(-3, 3))
local nightmare = SpawnPrefab("nightmarefuel")
nightmare.Transform:SetPosition((pos + offset):Get())
end
-- After taunt, return to idle
inst.AnimState:PushAnimation("idle", true)
end
end
-- Function to handle when the boss dies
local function OnDeath(inst)
-- Play death animation and sound
inst.AnimState:PlayAnimation("death")
inst.SoundEmitter:PlaySound(sounds.death)
-- Spawn loot
local pos = inst:GetPosition()
-- Always drop the horn and essence
local horn = SpawnPrefab("ancient_guardian_horn")
horn.Transform:SetPosition(pos:Get())
local essence = SpawnPrefab("ancient_essence")
essence.Transform:SetPosition(pos:Get())
-- Spawn additional loot
for i = 1, math.random(3, 6) do
local loot = SpawnPrefab(
weighted_random_choice({
nightmarefuel = 0.4,
thulecite_pieces = 0.3,
thulecite = 0.2,
purplegem = 0.1,
})
)
if loot then
local offset = Vector3(math.random(-2, 2), 0, math.random(-2, 2))
loot.Transform:SetPosition((pos + offset):Get())
end
end
-- Spawn death effect
SpawnPrefab("statue_transition_2").Transform:SetPosition(pos:Get())
end
-- Function to handle slam attack
local function DoSlamAttack(inst)
-- Play slam animation and sound
inst.AnimState:PlayAnimation("atk")
inst.SoundEmitter:PlaySound(sounds.slam)
-- Wait for the animation to reach the impact frame
inst:DoTaskInTime(0.5, function()
-- Apply damage in an area
local pos = inst:GetPosition()
local ents = TheSim:FindEntities(pos.x, pos.y, pos.z, 5, {"player", "character"}, {"playerghost", "INLIMBO"})
for _, ent in ipairs(ents) do
if ent and ent.components.health and not ent.components.health:IsDead() then
-- Apply damage and knockback
ent.components.health:DoDelta(-SLAM_DAMAGE * TUNING.ANCIENT_GUARDIAN.DAMAGE_MULT)
-- Knockback effect
if ent.Physics then
local angle = (ent:GetPosition() - pos):GetNormalized()
ent.Physics:SetVel(angle.x * 15, 6, angle.z * 15)
end
-- Screen shake for affected players
if ent.components.playercontroller then
ent.components.playercontroller:ShakeCamera(inst, "FULL", 0.7, 0.02, 1.5, 40)
end
end
end
-- Visual effect
SpawnPrefab("groundpoundring_fx").Transform:SetPosition(pos:Get())
end)
-- Return to idle after attack
inst.AnimState:PushAnimation("idle", true)
end
-- Function to handle charge attack
local function StartChargeAttack(inst)
-- Set up the charge
inst.AnimState:PlayAnimation("charge_pre")
inst.SoundEmitter:PlaySound(sounds.charge_pre)
-- Start the charge after the pre-animation
inst:DoTaskInTime(0.6, function()
inst.AnimState:PlayAnimation("charge", true)
inst.SoundEmitter:PlaySound(sounds.charge, "charging")
inst.components.locomotor:SetExternalSpeedMultiplier(inst, "charging", 2.5)
inst.charging = true
inst.charge_time = 3 -- Charge for 3 seconds
-- Set up collision damage
inst.collision_task = inst:DoPeriodicTask(0.1, function()
if inst.charging then
local pos = inst:GetPosition()
local ents = TheSim:FindEntities(pos.x, pos.y, pos.z, 3, {"player", "character"}, {"playerghost", "INLIMBO"})
for _, ent in ipairs(ents) do
if ent and ent.components.health and not ent.components.health:IsDead() then
-- Apply damage and knockback
ent.components.health:DoDelta(-CHARGE_DAMAGE * TUNING.ANCIENT_GUARDIAN.DAMAGE_MULT)
-- Knockback effect
if ent.Physics then
local angle = (ent:GetPosition() - pos):GetNormalized()
ent.Physics:SetVel(angle.x * 20, 5, angle.z * 20)
end
end
end
end
end)
-- End the charge after the duration
inst:DoTaskInTime(inst.charge_time, function()
if inst.charging then
inst.charging = false
inst.components.locomotor:RemoveExternalSpeedMultiplier(inst, "charging")
if inst.collision_task then
inst.collision_task:Cancel()
inst.collision_task = nil
end
inst.SoundEmitter:KillSound("charging")
inst.AnimState:PlayAnimation("charge_pst")
inst.AnimState:PushAnimation("idle", true)
end
end)
end)
end
local function fn()
local inst = CreateEntity()
inst.entity:AddTransform()
inst.entity:AddAnimState()
inst.entity:AddSoundEmitter()
inst.entity:AddDynamicShadow()
inst.entity:AddNetwork()
-- Set up physics
MakeCharacterPhysics(inst, 500, 1.5)
inst.Physics:SetCylinder(2, 3)
-- Set up animation
inst.AnimState:SetBank("ancient_guardian")
inst.AnimState:SetBuild("ancient_guardian")
inst.AnimState:PlayAnimation("idle", true)
-- Set up shadow
inst.DynamicShadow:SetSize(6, 3.5)
-- Add tags
inst:AddTag("epic")
inst:AddTag("monster")
inst:AddTag("hostile")
inst:AddTag("ancient_guardian")
inst:AddTag("largecreature")
inst:AddTag("boss")
-- Network variables
inst.entity:SetPristine()
if not TheWorld.ismastersim then
return inst
end
-- Add components
inst:AddComponent("health")
inst.components.health:SetMaxHealth(TUNING.ANCIENT_GUARDIAN.HEALTH)
inst.components.health.nofadeout = true
inst:AddComponent("combat")
inst.base_damage = 50 * TUNING.ANCIENT_GUARDIAN.DAMAGE_MULT
inst.components.combat:SetDefaultDamage(inst.base_damage)
inst.components.combat:SetAttackPeriod(3)
inst.components.combat:SetRange(3, 5)
inst.components.combat:SetRetargetFunction(3, function(inst)
return FindEntity(
inst,
30,
function(guy)
return inst.components.combat:CanTarget(guy)
end,
nil,
nil,
{"playerghost", "INLIMBO"}
)
end)
inst:AddComponent("lootdropper")
inst:AddComponent("inspectable")
inst.components.inspectable:SetDescription("An ancient stone guardian brought to life with dark energy.")
inst:AddComponent("locomotor")
inst.components.locomotor.walkspeed = 3
inst.components.locomotor.runspeed = 5
-- Set up special attacks
inst.DoSlamAttack = DoSlamAttack
inst.StartChargeAttack = StartChargeAttack
-- Set up state graph and brain
inst:SetStateGraph("SGancient_guardian")
inst:SetBrain(brain)
-- Set up event listeners
inst:ListenForEvent("attacked", OnHit)
inst:ListenForEvent("death", OnDeath)
-- Initial setup
inst.enraged = false
inst.charging = false
return inst
end
return Prefab("ancient_guardian", fn, assets, prefabs)
Step 3: Creating the State Graph
Now let's create the state graph that will control the boss's behaviors and animations:
scripts/stategraphs/SGancient_guardian.lua
require("stategraphs/commonstates")
local events = {
EventHandler("attacked", function(inst)
if not (inst.sg:HasStateTag("busy") or inst.sg:HasStateTag("attack") or inst.sg:HasStateTag("charging")) then
inst.sg:GoToState("hit")
end
end),
EventHandler("death", function(inst)
inst.sg:GoToState("death")
end),
EventHandler("doattack", function(inst, data)
if not (inst.sg:HasStateTag("busy") or inst.sg:HasStateTag("attack")) then
-- Choose between different attack types
local attack_type = math.random(1, 10)
if attack_type <= 5 then
-- Regular attack (50% chance)
inst.sg:GoToState("attack")
elseif attack_type <= 8 then
-- Slam attack (30% chance)
inst.sg:GoToState("slam")
else
-- Charge attack (20% chance)
inst.sg:GoToState("charge_pre")
end
end
end),
}
local states = {
State {
name = "idle",
tags = {"idle", "canrotate"},
onenter = function(inst)
inst.AnimState:PlayAnimation("idle", true)
inst.Physics:Stop()
end,
},
State {
name = "taunt",
tags = {"busy"},
onenter = function(inst)
inst.AnimState:PlayAnimation("taunt")
inst.Physics:Stop()
inst.SoundEmitter:PlaySound("ancientguardian/attack")
end,
events = {
EventHandler("animover", function(inst)
inst.sg:GoToState("idle")
end),
},
},
State {
name = "hit",
tags = {"busy"},
onenter = function(inst)
inst.AnimState:PlayAnimation("hit")
inst.Physics:Stop()
inst.SoundEmitter:PlaySound("ancientguardian/hurt")
end,
events = {
EventHandler("animover", function(inst)
inst.sg:GoToState("idle")
end),
},
},
State {
name = "death",
tags = {"busy"},
onenter = function(inst)
inst.AnimState:PlayAnimation("death")
inst.Physics:Stop()
inst.components.locomotor:StopMoving()
RemovePhysicsColliders(inst)
inst.SoundEmitter:PlaySound("ancientguardian/death")
end,
},
State {
name = "attack",
tags = {"attack", "busy"},
onenter = function(inst)
inst.AnimState:PlayAnimation("atk")
inst.Physics:Stop()
inst.components.locomotor:StopMoving()
inst.SoundEmitter:PlaySound("ancientguardian/attack")
end,
timeline = {
TimeEvent(20*FRAMES, function(inst)
inst.components.combat:DoAttack()
end),
},
events = {
EventHandler("animover", function(inst)
inst.sg:GoToState("idle")
end),
},
},
State {
name = "slam",
tags = {"attack", "busy"},
onenter = function(inst)
inst.DoSlamAttack(inst)
inst.Physics:Stop()
inst.components.locomotor:StopMoving()
end,
events = {
EventHandler("animover", function(inst)
inst.sg:GoToState("idle")
end),
},
},
State {
name = "charge_pre",
tags = {"attack", "busy", "charging"},
onenter = function(inst)
inst.StartChargeAttack(inst)
inst.Physics:Stop()
inst.components.locomotor:StopMoving()
end,
events = {
EventHandler("animover", function(inst)
inst.sg:GoToState("charge")
end),
},
},
State {
name = "charge",
tags = {"attack", "busy", "charging", "moving"},
onenter = function(inst)
inst.components.locomotor:RunForward()
end,
onupdate = function(inst)
-- Keep running forward during charge
inst.components.locomotor:RunForward()
-- Check for obstacles
local pos = inst:GetPosition()
local ahead = pos + (inst.Transform:GetRotation():ToVector3() * 2)
-- If we hit a solid obstacle, end the charge
if TheWorld.Map:IsVisualGroundAtPoint(ahead.x, ahead.y, ahead.z) == false then
inst.sg:GoToState("charge_pst")
end
end,
onexit = function(inst)
-- In case we exit the state without properly ending the charge
if inst.charging then
inst.charging = false
inst.components.locomotor:RemoveExternalSpeedMultiplier(inst, "charging")
if inst.collision_task then
inst.collision_task:Cancel()
inst.collision_task = nil
end
inst.SoundEmitter:KillSound("charging")
end
end,
},
State {
name = "charge_pst",
tags = {"busy"},
onenter = function(inst)
inst.AnimState:PlayAnimation("charge_pst")
inst.Physics:Stop()
inst.components.locomotor:StopMoving()
end,
events = {
EventHandler("animover", function(inst)
inst.sg:GoToState("idle")
end),
},
},
}
-- Add walking and running states from CommonStates
CommonStates.AddWalkStates(states, {
walktimeline = {
TimeEvent(0, function(inst) inst.SoundEmitter:PlaySound("ancientguardian/walk") end),
TimeEvent(12*FRAMES, function(inst) inst.SoundEmitter:PlaySound("ancientguardian/walk") end),
},
})
CommonStates.AddRunStates(states, {
runtimeline = {
TimeEvent(0, function(inst) inst.SoundEmitter:PlaySound("ancientguardian/walk") end),
TimeEvent(10*FRAMES, function(inst) inst.SoundEmitter:PlaySound("ancientguardian/walk") end),
},
})
return StateGraph("ancient_guardian", states, events, "idle")
Step 4: Creating the AI Brain
Now let's create the brain that will control the boss's decision making:
scripts/brains/ancient_guardian_brain.lua
require "behaviours/wander"
require "behaviours/chaseandattack"
require "behaviours/standstill"
require "behaviours/runaway"
require "behaviours/doaction"
require "behaviours/attackwall"
local AncientGuardianBrain = Class(Brain, function(self, inst)
Brain._ctor(self, inst)
end)
-- Parameters for behaviors
local MAX_CHASE_TIME = 10
local MAX_CHASE_DIST = 40
local WANDER_DIST = 20
local SEE_PLAYER_DIST = 30
local AGGRO_DIST = 15
-- Function to find nearby players
local function GetPlayerTarget(inst)
local nearest_player = nil
local nearest_dist = SEE_PLAYER_DIST * SEE_PLAYER_DIST
for i, v in ipairs(AllPlayers) do
if v and v:IsValid() and not v:HasTag("playerghost") then
local dist = inst:GetDistanceSqToInst(v)
if dist < nearest_dist then
nearest_dist = dist
nearest_player = v
end
end
end
-- If player is close enough to aggro, return them as target
if nearest_player and inst:GetDistanceSqToInst(nearest_player) < AGGRO_DIST * AGGRO_DIST then
return nearest_player
end
return nil
end
-- Function to check if the boss should use a special attack
local function ShouldSpecialAttack(inst)
-- Only use special attacks if enraged or at random times
if inst.enraged or math.random() < 0.2 then
if inst.components.combat.target ~= nil then
-- Make sure target is valid
if inst.components.combat.target:IsValid() and not inst.components.combat.target:HasTag("playerghost") then
-- Check if target is in range
local dist = inst:GetDistanceSqToInst(inst.components.combat.target)
if dist < 100 then -- 10 units squared
return true
end
end
end
end
return false
end
function AncientGuardianBrain:OnStart()
local root = PriorityNode({
-- If we have a wall in the way, attack it
AttackWall(self.inst),
-- If we should do a special attack, do it
WhileNode(function() return ShouldSpecialAttack(self.inst) end, "Special Attack",
ActionNode(function()
-- The actual attack type is chosen in the stategraph
self.inst:PushEvent("doattack")
return SUCCESS
end)),
-- Chase and attack any player that gets too close
ChaseAndAttack(self.inst, MAX_CHASE_TIME, MAX_CHASE_DIST),
-- Look for players to target
WhileNode(function() return self.inst.components.combat.target == nil end, "Find Target",
ActionNode(function()
local target = GetPlayerTarget(self.inst)
if target ~= nil then
self.inst.components.combat:SetTarget(target)
end
return SUCCESS
end)),
-- If nothing else to do, wander around
Wander(self.inst, function()
-- Try to stay near the original spawn point if we have one
if self.inst.spawn_point ~= nil then
return self.inst.spawn_point
end
return self.inst:GetPosition()
end, WANDER_DIST),
}, .25)
self.bt = BT(self.inst, root)
end
function AncientGuardianBrain:OnUpdate()
-- Special behavior when enraged
if self.inst.enraged then
-- If we have a target, occasionally taunt
if self.inst.components.combat.target ~= nil and math.random() < 0.01 then
self.inst:PushEvent("taunt")
end
end
end
return AncientGuardianBrain
Step 5: Creating the Loot Items
Now let's create the special items that the boss will drop when defeated:
scripts/prefabs/ancient_guardian_horn.lua
local assets = {
Asset("ANIM", "anim/ancient_guardian_horn.zip"),
Asset("ATLAS", "images/inventoryimages/ancient_guardian_horn.xml"),
Asset("IMAGE", "images/inventoryimages/ancient_guardian_horn.tex"),
}
local function OnEquip(inst, owner)
owner.AnimState:OverrideSymbol("swap_object", "ancient_guardian_horn", "swap_ancient_guardian_horn")
owner.AnimState:Show("ARM_carry")
owner.AnimState:Hide("ARM_normal")
-- Grant bonuses when equipped
if owner.components.combat ~= nil then
owner.components.combat.externaldamagemultipliers:SetModifier(inst, 1.25)
end
if owner.components.locomotor ~= nil then
owner.components.locomotor:SetExternalSpeedMultiplier(inst, "ancient_horn", 1.1)
end
end
local function OnUnequip(inst, owner)
owner.AnimState:Hide("ARM_carry")
owner.AnimState:Show("ARM_normal")
-- Remove bonuses when unequipped
if owner.components.combat ~= nil then
owner.components.combat.externaldamagemultipliers:RemoveModifier(inst)
end
if owner.components.locomotor ~= nil then
owner.components.locomotor:RemoveExternalSpeedMultiplier(inst, "ancient_horn")
end
end
local function fn()
local inst = CreateEntity()
inst.entity:AddTransform()
inst.entity:AddAnimState()
inst.entity:AddNetwork()
MakeInventoryPhysics(inst)
inst.AnimState:SetBank("ancient_guardian_horn")
inst.AnimState:SetBuild("ancient_guardian_horn")
inst.AnimState:PlayAnimation("idle")
-- Make it a weapon
inst:AddTag("sharp")
inst:AddTag("weapon")
inst.entity:SetPristine()
if not TheWorld.ismastersim then
return inst
end
-- Add components
inst:AddComponent("weapon")
inst.components.weapon:SetDamage(60)
inst.components.weapon:SetRange(1.2)
inst:AddComponent("finiteuses")
inst.components.finiteuses:SetMaxUses(200)
inst.components.finiteuses:SetUses(200)
inst.components.finiteuses:SetOnFinished(function(inst) inst:Remove() end)
inst:AddComponent("inspectable")
inst.components.inspectable:SetDescription("A massive horn from the Ancient Guardian. It's imbued with power.")
inst:AddComponent("inventoryitem")
inst.components.inventoryitem.atlasname = "images/inventoryimages/ancient_guardian_horn.xml"
inst:AddComponent("equippable")
inst.components.equippable:SetOnEquip(OnEquip)
inst.components.equippable:SetOnUnequip(OnUnequip)
-- Special ability: Ground slam
inst:AddComponent("aoetargeting")
inst.components.aoetargeting.reticule.reticuleprefab = "reticuleaoe"
inst.components.aoetargeting.reticule.pingprefab = "reticuleaoeping"
inst.components.aoetargeting.reticule.targetfn = function() return inst:GetPosition() end
inst.components.aoetargeting.reticule.validcolour = { 1, .75, 0, 1 }
inst.components.aoetargeting.reticule.invalidcolour = { .5, 0, 0, 1 }
inst.components.aoetargeting.reticule.ease = true
inst.components.aoetargeting.reticule.mouseenabled = true
-- Add special attack function
inst.GroundSlam = function(inst, pos)
-- Create visual effect
SpawnPrefab("groundpoundring_fx").Transform:SetPosition(pos:Get())
-- Play sound effect
inst.SoundEmitter:PlaySound("ancientguardian/slam")
-- Apply damage to nearby entities
local ents = TheSim:FindEntities(pos.x, pos.y, pos.z, 5, nil, {"player", "playerghost", "INLIMBO"})
for _, ent in ipairs(ents) do
if ent and ent.components.health and not ent.components.health:IsDead() then
-- Apply damage
ent.components.health:DoDelta(-30)
-- Knockback effect
if ent.Physics then
local angle = (ent:GetPosition() - pos):GetNormalized()
ent.Physics:SetVel(angle.x * 10, 5, angle.z * 10)
end
end
end
-- Use durability
inst.components.finiteuses:Use(5)
end
-- Add special attack action
inst:AddComponent("spellcaster")
inst.components.spellcaster:SetSpellFn(function(inst, pos)
inst.GroundSlam(inst, pos)
return true
end)
inst.components.spellcaster.canuseonpoint = true
inst.components.spellcaster.canusefrominventory = false
MakeHauntableLaunch(inst)
return inst
end
return Prefab("ancient_guardian_horn", fn, assets)
scripts/prefabs/ancient_essence.lua
local assets = {
Asset("ANIM", "anim/ancient_essence.zip"),
Asset("ATLAS", "images/inventoryimages/ancient_essence.xml"),
Asset("IMAGE", "images/inventoryimages/ancient_essence.tex"),
}
local function fn()
local inst = CreateEntity()
inst.entity:AddTransform()
inst.entity:AddAnimState()
inst.entity:AddNetwork()
inst.entity:AddLight()
MakeInventoryPhysics(inst)
inst.AnimState:SetBank("ancient_essence")
inst.AnimState:SetBuild("ancient_essence")
inst.AnimState:PlayAnimation("idle", true)
-- Add light
inst.Light:SetFalloff(0.7)
inst.Light:SetIntensity(0.5)
inst.Light:SetRadius(1)
inst.Light:SetColour(0.5, 0.8, 1)
inst.Light:Enable(true)
-- Add tags
inst:AddTag("ancient")
inst:AddTag("molebait")
inst.entity:SetPristine()
if not TheWorld.ismastersim then
return inst
end
-- Add components
inst:AddComponent("inspectable")
inst.components.inspectable:SetDescription("A mysterious essence from the Ancient Guardian. It pulses with energy.")
inst:AddComponent("inventoryitem")
inst.components.inventoryitem.atlasname = "images/inventoryimages/ancient_essence.xml"
inst:AddComponent("stackable")
inst.components.stackable.maxsize = 20
-- Add fuel component for magic items
inst:AddComponent("fuel")
inst.components.fuel.fueltype = FUELTYPE.NIGHTMARE
inst.components.fuel.fuelvalue = TUNING.LARGE_FUEL
-- Add tradable component
inst:AddComponent("tradable")
inst.components.tradable.goldvalue = 10
-- Special effect: Sanity aura when carried
inst:AddComponent("equippable")
inst.components.equippable.equipslot = EQUIPSLOTS.BODY
inst:AddComponent("sanityaura")
inst.components.sanityaura.aura = TUNING.SANITYAURA_TINY
MakeHauntableLaunch(inst)
return inst
end
return Prefab("ancient_essence", fn, assets)
Step 6: Testing and Debugging
To test your mod:
- Place your mod folder in the Don't Starve Together mods directory
- Enable the mod in the game's mod menu
- Start a new game and use the console command
c_spawn("ancient_guardian")
to spawn the boss - Test the boss's behaviors, attacks, and drops
Common Issues and Solutions:
- Animation Errors: Ensure all animation files are properly created and referenced
- AI Not Working: Check for errors in the brain file and make sure behavior functions are properly defined
- Special Attacks Not Working: Verify that the stategraph is correctly handling the attack events
- Loot Not Dropping: Check that the OnDeath function is properly spawning the loot items
- Performance Issues: If the boss causes lag, consider optimizing the special effects or collision checks
Debugging Tips:
- Use
print()
statements to track the execution flow of your code - Check the log file for error messages
- Use the console command
c_select()
to inspect entity properties - Use
c_godmode()
to test boss behaviors without dying
Step 7: Extending the Mod
Here are some ideas for extending this mod:
- Add a ritual altar that players can build to summon the boss
- Create additional boss phases with new attack patterns
- Add special environmental effects when the boss is enraged
- Create a quest system related to the boss
- Add more unique loot items with special abilities
- Create minions that the boss can summon during the fight
Conclusion
Congratulations! You've created a complete mod that adds a custom boss to Don't Starve Together. This project demonstrates many important modding concepts:
- Creating complex entities with custom AI
- Implementing special attacks and abilities
- Using state graphs for entity behavior
- Creating custom items with unique effects
- Integrating with the game's existing systems
This boss mod provides a challenging new encounter for players and rewards them with powerful unique items. You can expand on this foundation to create even more complex boss encounters or integrate this boss into a larger mod with additional content.
Remember to thoroughly test your mod before publishing it to ensure a smooth experience for players. Consider gathering feedback from players to refine and improve your boss's mechanics and balance.