Terraria's Destroyer Mechanical Boss (Worm AI, Free Model Released + Instructions)

I just finished making a worm AI script that is compatible with both filtering modes that is capable of tunneling through bricks as well as tunneling through air when under a certain height. Right now it is using a model of the Destroyer, a mid to late game boss in the game Terraria. With the current setup it has a total of 82 segments, the same amount of segments it has in Terraria, and does the same amount of damage as well. The script is can support any number of worms with any number of segments[strike], but most of the settings are global in it right now so all worms would behave exactly the same[/strike] and now the script uses template tables full of variables. Each worm, even ones of the same type, can have its own unique values. The worms have Humanoids in them so any Humanoid damaging weapon will work against them. There are a lot of variables that can be changed to customize the behavior of the worms. It is currently on display at this place.

Once I am done fixing bugs with the script and polish it a bit, I will release a copy of it in a free model for everyone to use. Before I do that though, I am going to need to refactor code, add comments, and add support for multiple worm configurations. If you find any bugs, just let me know and I will do my best to fix them. Free model is here. Now that the model is released, show me what you can make with this!

defaultConfigurationTable = {
	SegmentCount = 8,							--Number of body segments in the worm
	Health = 50,								--Maximum health of the worm
	HeadContactDamage = 3.5,					--Damage dealt by the worm's head segment
	BodyContactDamage = 2.5,					--Damage dealt by the worm's body segments
	TailContactDamage = 2,						--Damage dealt by the worm's tail segment
	GroundHeight = 0,							--Height where everything is considered as "ground" by the AI
	MinimumTravelDepth = -1e6,					--Depth where the worm avoids reaching (will automatically change to Workspace.FallenPartsDestroyHeight + 10 if below it)
	AttackDepth = 100,							--How far below the target the worm travels before charging at the target
	Speed = 50,									--Maximum movement speed of worm (will slow down if too much speed is gained while in the air)
	Acceleration = 25,							--Acceleration rate towards target destination
	Gravity = 10,								--Downwards acceleration due to gravity
	MinimumAttackSpeed = 40,					--Movement speed towards the target required to surface
	SegmentPartSize = Vector3.new(1,1,1),		--Size of each segment part
	MinimumDamageSpeed = 10,					--Minimum speed required for segments to damage objects
	HeadSegmentModel = destroyerSegmentModels.Head,		--Model for the worm's head segment
	BodySegmentModel = destroyerSegmentModels.Segment,	--Model for the worm's body segment
	TailSegmentModel = destroyerSegmentModels.Tail,		--Model for the worm's tail segment
	UpdateFrameSkip = 3,						--How many frames the worm AI should skip target position updates
	
	--[[Finds a new target when the current target is lost
		Parameters:
			table worm - Contains AI data for the worm. Table includes all configuration data for its AI type as well as:
				Model Model - A model containing all of the server sided segments. All segments are invisible and not collidable
				table Segments - A table containing all of the segment parts
				BasePart Target - The part that the worm moves towards
	]]
	FindNewTarget = function(wormAI) end,
	
	--[[
		Re-evaluates the current target to check if the target is valid. Returns true if the target is valid, returns false if it isn't.
		Parameters:
			table worm - Contains AI data for the worm. Table includes all configuration data for its AI type as well as:
				Model Model - A model containing all of the server sided segments. All segments are invisible and not collidable
				table Segments - A table containing all of the segment parts
				BasePart Target - The part that the worm moves towards
	]]
	ReevaluateTarget = function(wormAI) end,
	
	--[[
		Fired when the worm hits an object.
		Parameters:
			table wormAI - Contains AI data for the worm. Table includes all configuration data for its AI type as well as:
				Model Model - A model containing all of the server sided segments. All segments are invisible and not collidable
				table Segments - A table containing all of the segment parts
				BasePart Target - The part that the worm moves towards
			Part segment - The segment that hit the object.
			BasePart object - The object the segment hit.
	]]
	HitObject = function(wormAI,segment,object)end
}

[size=2]save yourself the headache and just copy and paste this to a text editor if you want to see this[/size]

[ol]
[li]Use require() on the WormAI ModuleScript and save the table returned to a variable.[/li]

[li]To add a new worm AI template, call RegisterAIType(table variableOverwrites) in the table from step 1 and pass a table containing variable overwrite values to it. The template will be named after the “Name” field in the overwrite table that was passed and will be used when spawning new worms. Make sure you overwrite the SegmentPartSize, HeadSegmentModel, BodySegmentModel, and TailSegmentModel variables if you wish to use a new model for the worm.[/li]

[li]To spawn a new worm from a template, call NewWorm(value name,Vector3 position) and pass the name of the template as well as a desired spawn position. The function will return a table containing a copy of all the variables along with a few extras that are listed in the above spoiler.[/li]
[/ol]

That is all you have to do to create your own customized worm! There are a few callback functions that can be overwritten to customize the behavior further. I may be adding more callbacks that can be overwritten in the future. Since the model is open source you can just modify the AI script if you really need to change currently unchangeable behavior.

Example usage:

wormAI = require(game.ServerScriptService.WormAI)
dogeSegmentModel = game.ReplicatedStorage.DogeWormSegments
wormAI.RegisterAIType {
	Name = "DogeWorm",
	SegmentCount = 100,
	HeadSegmentModel = dogeSegmentModel.Head,
	BodySegmentModel = dogeSegmentModel.Segment,
	TailSegmentModel = dogeSegmentModel.Tail,
	Gravity = 50,
	HitObject = function(wormAI,segment,object)
		print("Wow!")
	end
}
dogeWormAI = wormAI.NewWorm("DogeWorm",Vector3.new(0,10,0))
dogeWormAI.Speed = 100

Note about requiring by model ID: I cannot make this model able to be required using the ID alone because the worm AI uses a LocalScript to display worms and it must be added to a location where the script will not be lost or constantly replaced (such as the StarterGui in games with RestartPlayerGuiOnSpawn set to true) and there is no guarantee the script will make it to ReplicatedFirst before the first player joins.

Note about building a model for a worm: To make a working model, just build one copy of a head, body, and tail segment and group each segment into their own Model object. Each segment model must have a “Main” part that the segment is centered around. The front side of the “Main” part connects to the segment in front, and the back side connects to the segment in the back. It is recommended to make all parts note collide-able and anchored or else (more) lag and/or weird behavior may occur.

5 Likes

3D terraria, anybody? :stuck_out_tongue:

1 Like

Sick!

1 Like

This is really neat, but as mentioned while I was checking this out, it’s pretty slow.

1 Like

So I got this error in my local console:

And this happened:

The worm stopped moving after that. I think it went too low and the head was deleted by the FallenPartsDestroyHeight (defaults to -500)

I think this kind of curving and how high it goes might be unintentional (click for full size):

I also got 30 FPS the entire time the worm was moving, and I doubt many people would want to use it in their places if on an empty baseplate it gives 30 FPS. You might want to try optimizing it a little.

For reference, my system specs are:
3.9 GHz quad core CPU
16 GB RAM
4 GB VRAM

[quote] So I got this error in my local console:

And this happened:

The worm stopped moving after that. I think it went too low and the head was deleted by the FallenPartsDestroyHeight (defaults to -500)

I think this kind of curving and how high it goes might be unintentional (click for full size):

I also got 30 FPS the entire time the worm was moving, and I doubt many people would want to use it in their places if on an empty baseplate it gives 30 FPS. You might want to try optimizing it a little.

For reference, my system specs are:
3.9 GHz quad core CPU
16 GB RAM
4 GB VRAM [/quote]

I don’t think speed will be an issue in any use case other than this one. The Destroyer is made up of 82 segments, with most of the segments containing 15 parts each plus a server sided part to detect collisions. Also, the frame rate doesn’t drop too much when I spawn 2 or even 3 of them so I am sure that with smaller worms there will be little to no performance impact.

About the missing HeadSegment error, yeah, that happens sometimes. It is a quick fix though because at that point it is trying to get some distance from the player by heading straight in whatever direction it is going in, and I can make sure that it is never going nearly straight down with a randomized offset.

And when did that curving happen? What was the behavior of the head when it happened? It detects if it is in ground and can change direction using IsRegion3EmptyWithIgnoreList or if it is under a certain height, so it might have detected something to make it think “oh hey, ground here!”

The head is at the top of that picture. It had gone completely below the baseplate, then the head went up, then the whole worm came out of the ground and went pretty high, and then it dove down and got its head destroyed.

There also seems to be a weird behavior difference between studio tests and live server tests. The worm likes to go straight down more in live servers and is a lot less accurate when it charges up at the player. I’m not really sure what is causing this difference in behavior. Anyone know why? Here is the code that handles the path the head segment takes. Btw, it determines the target by detecting the closest living player if it has no target.

if IsSegmentInGround(headSegment) then
	local targetPosition
	if destroyer.Target and destroyer.Target:FindFirstChild("Torso") then

		--State 1 is just directly moving towards the target
		targetPosition = destroyer.Target.Torso.Position
		local distanceFromTarget = (destroyer.Target.Torso.Position - headSegment.Position).magnitude
		local surfaceDistanceFromTarget = ((destroyer.Target.Torso.Position - headSegment.Position) * Vector3.new(1,0,1)).magnitude
		local desiredSpeedGain = (headSegment.BodyVelocity.Velocity - ((targetPosition - headSegment.Position).unit * speed)).magnitude

		if desiredSpeedGain>speed and distanceFromTarget/headSegment.BodyVelocity.Velocity.magnitude<(minimumAttackSpeed/acceleration)+1 then

			--State 2, moves away from the target
			targetPosition = headSegment.Position + headSegment.CFrame.lookVector * 100
		elseif surfaceDistanceFromTarget/headSegment.BodyVelocity.Velocity.magnitude>(minimumAttackSpeed/acceleration)+1 then

			--State 3, moves somewhere below the target to surface at an angle
			targetPosition = targetPosition - Vector3.new(0,100,0)
		end
	else
		targetPosition = Vector3.new(0,50,0)
	end
	local optimalDirection = (targetPosition - headSegment.Position).unit * speed
        local optimalAcceleration = (optimalDirection - headSegment.BodyVelocity.Velocity).unit
	headSegment.BodyVelocity.Velocity = headSegment.Velocity + (optimalAcceleration * acceleration * delta)
else
	headSegment.BodyVelocity.Velocity = headSegment.BodyVelocity.Velocity - (Vector3.new(0,gravity,0) * delta)
end

Diagram of behavior differences:

Update: a recent update eliminated this weird behavior online

I am almost done preparing the script so it can be published as a model. Before I do that though, I want to try to optimize the code for displaying the worm’s body as much as I can, but the problem is that I can’t think of any ways to optimize it more. Any help optimizing it would be appreciated. Here is the problem code:

function MoveModelToCFrame(model,cframe)
	local mainCFrame = model.Main.CFrame
	for count,part in pairs(model:GetChildren()) do
		part.CFrame = part.CFrame==mainCFrame and cframe or (cframe * mainCFrame:inverse() * part.CFrame)
	end
end

--This bit happens once every 2 frames for every worm AI
for segmentCount,segmentModel in pairs(worm.SegmentModels) do
	local segment = worm.ModelChildren[segmentCount + 4]
	local segmentPosition = segment.Position
	local previousSegmentPosition = worm.ModelChildren[segmentCount + 3].Position
	local segmentPositionDifference = previousSegmentPosition - segmentPosition
	if segmentPositionDifference.magnitude>segment.Size.Y*0.9 then
		local segmentCFrame = CFrame.new(previousSegmentPosition - (segmentPositionDifference.unit * segment.Size.Y * 0.95),previousSegmentPosition)
		MoveModelToCFrame(segmentModel,segmentCFrame)
		if workspace.FilteringEnabled then
			segment.CFrame = segmentCFrame
		end
	end
end

Update: Swapping out my custom set Model CFrame function for Model:SetPrimaryPartCFrame made it run 2x faster. I can’t believe that I forgot about that method.

I have never seen anyone else making something like this. Really cool!
Reminds me of the worm boss fight from Rayman Legends

One small suggestion is that you should decrease the cooldown time from the worm starting to turning around again to attack you.

[quote] I have never seen anyone else making something like this. Really cool!
Reminds me of the worm boss fight from Rayman Legends

One small suggestion is that you should decrease the cooldown time from the worm starting to turning around again to attack you. [/quote]

I’ll see what I can do. The solution for making the worm decide what to do is a bit messy, so it is going to be a bit tough. I should probably switch the current code out for a full blown state system to simplify the behavior.

This Is 5 years Old! I am surprised that it still mostly works, well sometimes. The Head is completely gone and sometimes the segments are separated. But The Laser Still Shoot and I have some very Nice Photos of me testing it.


EDIT I just notice that The head does spawn in but it flys away not connected.

2 Likes

Oh my. This is 9 YEARS OLD and it still works amazingly. Not that you’re going to respond (you’ve probably moved away from Roblox it’s been that long) but if any of you know how to fix the worm not damaging the player when it hits would be awesome