Transformation System with Shadow Form Template + Free Crawling Animations

I wrote this algorithm today to manage transformations.
The transformation included in the model is called “Shadow Form”. It is triggered when you have 1 health and you cannot equip tools while you are in this form and you crawl.
I designed this as a system to allow support for other transformations and easy code refactoring into a module.

This is the source code of the transformation system.

local Character=script.Parent
local formname="Form"
local Forms={}
local function Form(Character,FormName,bool)
	local FormObject=Character:FindFirstChild(FormName) or Instance.new("BoolValue")
	if FormObject.Name~=FormName then
		FormObject.Name=FormName
		FormObject.Parent=Character
	end
	if bool then
	FormObject.Value=bool
	end
	return FormObject
end
local FormData={}
local animcache=script.AnimationCache
local function ReplaceAnimations(library,AnimationScript)
	local payload={}
	local objects={}
	for i,v in library:GetChildren() do
		local cachethis= AnimationScript:FindFirstChild(v.Name)
		if cachethis then		
			table.insert(payload,cachethis)
			cachethis.Parent=animcache
			v.Parent=AnimationScript
			table.insert(objects,v)
		end
	end
	return {payload=payload,objects=objects}
end
local faces={"Front","Left","Right","Bottom","Top","Back"}
local ts=game:GetService("TweenService")
local debris=game:GetService("Debris")
local imagetrans=TweenInfo.new(5)

local function covertexture(imageobject,subject)
	local images={}
	for i,v in faces do
		local imageclone=imageobject:Clone()
		imageclone.Face=v
		imageclone.Transparency=1
		imageclone.Parent=subject
		table.insert(images,imageclone)
		ts:Create(imageclone,imagetrans,{Transparency=0.3}):Play()
	end
	return images
end

local function playsound(sound)
	sound.Volume=0
	sound.Pitch=math.random(75,125)/100
	sound.PlaybackSpeed=math.random(75,125)/100
	--sound.Parent=Character.PrimaryPart
	sound:Play()
	ts:Create(sound,imagetrans,{Volume=math.random(30,100)/100}):Play()
	
end
FormData={
	["Dark Form"]={
		[true]=function(Character)
			local payload={}
			local colorhash={}
			local texturepayload={}
			local particlepayload={}
			
			for i,v in Character:GetChildren() do 
				if v:IsA("BasePart") and v.Transparency~=1 then
					table.insert(payload,{object=v,Color=v.Color})
					ts:Create(v,imagetrans,{Color=Color3.fromRGB(0,0,0)}):Play()
					colorhash[v.Name]=true	
					for t,o in script["Dark Form Particles"]:GetChildren() do 
						local c=o:Clone()
						c.Parent=v
						c.Enabled=true
						table.insert(particlepayload,c)
					end
				end
			end
			local sound=script["Dark Form Sounds"]["Hollow Rumble 1 (SFX)"]:Clone()
			sound.Parent=Character.PrimaryPart
			playsound(sound)
			for i,v in Character:GetDescendants() do
				if v:IsA("BasePart") and v.Transparency~=1 and colorhash[v.Name]==nil then
					table.insert(texturepayload,covertexture(script.Black,v))
				end
			end 
			local animpayload=ReplaceAnimations(script["Dark Form"],Character.Animate)
			FormData["Dark Form"].Data["Colors"]=payload
			FormData["Dark Form"].Data["Animations"]=animpayload
			FormData["Dark Form"].Data["Textures"]=texturepayload
			FormData["Dark Form"].Data["Particles"]=particlepayload
			FormData["Dark Form"].Data["Sounds"]={sound}
			end,
		[false]=function(Character)
			if FormData["Dark Form"].Data.Colors and #FormData["Dark Form"].Data.Colors>1 then
			for i,v in FormData["Dark Form"].Data.Colors do 
				ts:Create(v.object,imagetrans,{Color=v.Color}):Play()
			end	
			for i,v in FormData["Dark Form"].Data.Textures do 
				for t,o in v do 
				ts:Create(o,imagetrans,{Transparency=1}):Play()
				debris:AddItem(o,7)
				end
			end
			FormData["Dark Form"].Data.Textures={}
			for i,v in FormData["Dark Form"].Data.Sounds do 
					ts:Create(v,imagetrans,{Volume=0}):Play()
					debris:AddItem(v,8)
			end	
			FormData["Dark Form"].Data.Sounds={}
			for i,v in FormData["Dark Form"].Data.Particles do 
				v.Enabled=false 
				debris:AddItem(v,5)
				--v:Destroy()
			end
			
			FormData["Dark Form"].Data.Particles={}
			FormData["Dark Form"].Data.Colors={}
			local v =FormData["Dark Form"].Data.Animations-- do 
				for t,o in v.payload do 
					o.Parent=Character.Animate
				end
				for t,o in v.objects do 
					o.Parent=script["Dark Form"]
				end
			--end	
			FormData["Dark Form"].Data.Animations={}
		end 
		end,
		["Data"]={},
		Connections=function(Character)
			local a=Character.ChildAdded:Connect(function(v)
				if v:IsA("Tool") then
					Character.Humanoid:UnequipTools()
				end
			end)
			local b=Character.Humanoid:GetPropertyChangedSignal("Health"):Connect(function()
			if Character.Humanoid.Health<=1 then
				Character.Humanoid.Health=1				
				Forms.Toggle(Character,"Dark Form",true)
			elseif Character.Humanoid.Health>=Character.Humanoid.MaxHealth*.25 then
				Forms.Toggle(Character,"Dark Form",false)
			end
			end)
			return {a,b}
		end,
	},	
}

function Forms.Toggle(Character,FormName,bool)
	local formobject=Form(Character,FormName)
	if bool==nil then
	if formobject.Value==false then
		bool=true
		formobject.Value=true
	else bool=false 	
	end
	--FormData[FormName][bool](Character)
	elseif formobject==bool then
		return 
	end
		FormData[FormName][bool](Character)
	
end
function Forms.Initalize(Character,FormHash)
local connections={}
for i,v in FormData do
	if FormHash==nil or FormHash[i]~=nil then
	Form(Character,formname)
	table.insert(connections,v.Connections(Character))
	end
end
local function disconnect()
	for i,group in connections do
		for t,connection in group do 
			connection:Disconnect()
		end
	end
end
table.insert(connections,Character.AncestryChanged:Connect(function()
	if Character.Parent~=workspace then
		disconnect()
	end
end))

end


image
image
image

This is the model you can try out by placing it in StarterCharacterScripts
PlaceScriptIn_StarterCharacterScripts - Creator Store (roblox.com)

If you want to use the animations you can import them with my free plugin bulk animation importer.
Bulk Animation Importer - Creator Store (roblox.com)

It is based off Sora’s Shadow form design from Kingdom Hearts II

6 Likes

I published an update to the code fixing a bug with the form toggle function This is the changes

function Forms.Toggle(Character,FormName,bool)
	local formobject=Form(Character,FormName)
	if bool==nil then
	if formobject.Value==false then
		bool=true
		formobject.Value=true
	else 
		formobject.Value=false
		bool=false 	
	end
	elseif formobject.Value==bool then
		return 			
	end
	formobject.Value=bool
	FormData[FormName][bool](Character)
	
end

This function ensures that a trigger is not called twice while it is on.

Absolutely amazing appreciate this

1 Like

I’m glad you like it! I have updated it to my newest version!
Thew newest version features new seamless saving and loading data!
You can now set Conditions for a transformation or not set conditions. These conditions are designed to manage transformations that do not overlap each other.

print("Hello world!")
local Character=script.Parent
local Forms={}
local FormData={}
local animcache=script.AnimationCache

local faces={"Front","Left","Right","Bottom","Top","Back"}
local ts=game:GetService("TweenService")
local debris=game:GetService("Debris")
local imagetrans=TweenInfo.new(5)
local Library=game.ReplicatedStorage.Spellbook.SpellAssets

local DataStoreService = game:GetService("DataStoreService")
local TransformationsDataStore = DataStoreService:GetDataStore("TransformationsDataStore")

local function saveTransformations(userId, transformationsTable)
	local success, errorMessage = pcall(function()
		TransformationsDataStore:SetAsync(userId, transformationsTable)
	end)

	if success then
		print("Transformations saved successfully for user: " .. userId)
	else
		warn("Failed to save transformations for user: " .. userId .. ". Error: " .. errorMessage)
	end
end

local function loadTransformations(userId)
	local success, transformationsTable = pcall(function()
		return TransformationsDataStore:GetAsync(userId)
	end)

	if success then
		if transformationsTable then
			print("Transformations loaded successfully for user: " .. userId)
			return transformationsTable
		else
			print("No transformations found for user: " .. userId)
			return nil--{}
		end
	else
		warn("Failed to load transformations for user: " .. userId .. ". Error: " .. transformationsTable)
		return nil--{}
	end
end


local function tweenhandle(object,ti)
	local initial=object.Handle.Transparency
	object.Handle.Transparency=1
	ts:Create(object.Handle,ti,{Transparency=initial}):Play()
end
local function Form(Character,FormName,bool)
	local FormObject=Character:FindFirstChild(FormName) or Instance.new("BoolValue")
	if FormObject.Name~=FormName then
		FormObject.Name=FormName
		FormObject.Parent=Character
	end
	if bool then
		FormObject.Value=bool
	end
	return FormObject
end
local function ReplaceAnimations(library,AnimationScript)
	local payload={}
	local objects={}
	for i,v in library:GetChildren() do
		local cachethis= AnimationScript:FindFirstChild(v.Name)
		if cachethis then		
			table.insert(payload,cachethis)
			cachethis.Parent=animcache
			v.Parent=AnimationScript
			table.insert(objects,v)
		end
	end
	return {payload=payload,objects=objects}
end

local function covertexture(imageobject,subject)
	local images={}
	for i,v in faces do
		local imageclone=imageobject:Clone()
		imageclone.Face=v
		imageclone.Transparency=1
		imageclone.Parent=subject
		table.insert(images,imageclone)
		ts:Create(imageclone,imagetrans,{Transparency=0.3}):Play()
	end
	return images
end

local function playsound(sound)
	sound.Volume=0
	sound.Pitch=math.random(75,125)/100
	sound.PlaybackSpeed=math.random(75,125)/100
	sound:Play()
	ts:Create(sound,imagetrans,{Volume=math.random(30,100)/100}):Play()

end


local function HideParts(ti,object)--FormData.Butterfly.Data.Objects[1].Handle.Butterfly
	for i,v in object:GetDescendants() do
		if v:IsA("BasePart") then
			ts:Create(v,ti,{Transparency=1}):Play()
		end
	end
end

local function destroythings(TableObjects)
	for t,o in TableObjects do 				
		o:Destroy()
	end
end

local function ModelAccessoryAddTransition(object,objective,Character)
	--local objective=object.Handle:FindFirstChildOfClass("Model")
	local currentscale = objective:GetScale()
	if Character.Humanoid:FindFirstChild("BodyHeightScale") then
		currentscale*=Character.Humanoid.BodyHeightScale.Value
	end
	objective:ScaleTo(0.01)
	Character.Humanoid:AddAccessory(object)
	local steps = 15
	local scaleIncrement = (currentscale - 0.01) / steps
	local ti=TweenInfo.new(.3)

	for i,v in objective:GetDescendants() do
		if v:IsA("BasePart") then
			local goal={Transparency=v.Transparency}
			v.Transparency=1
			ts:Create(v,ti,goal):Play()
		end
	end
	task.spawn(function()
		for i = 1, steps do
			task.wait(0.03) -- Adjust the wait time as needed for the desired speed
			local newScale = 0.01 + (scaleIncrement * i)
			if objective.Parent==nil then
				break
			end
			objective:ScaleTo(newScale)
		end
	end)
end


--[[["Template"]={
--	[true]=function(Character)

--	end,
--	[false]=function(Character)

--	end,
--	["Data"]={},
--	Connections=function(Character)

--	end,
--}]]
local function CheckConditions(conditionarray,Character)
	for key,condition in conditionarray do 
		local formobject=Form(Character,key)
		if formobject.Value~=condition then			
			return false
		end
	end
	return true
end

FormData={
	["Dark Form"]={
		initial=true,
		--Conditions={
		--	["Dark Form"]=false,
		--},
		[true]=function(Character)
			local payload={}
			local colorhash={}
			local texturepayload={}
			local particlepayload={}
			
			for i,v in Character:GetChildren() do 
				if v:IsA("BasePart") then
					table.insert(payload,{object=v,Color=v.Color})
					ts:Create(v,imagetrans,{Color=Color3.fromRGB(0,0,0)}):Play()
					colorhash[v.Name]=true	
					if v.Transparency~=1 then
					for t,o in script["Dark Form Particles"]:GetChildren() do 
						local c=o:Clone()
						c.Parent=v
						c.Enabled=true
						table.insert(particlepayload,c)
					end
					end
				end
			end
			local sound=script["Dark Form Sounds"]["Hollow Rumble 1 (SFX)"]:Clone()
			sound.Parent=Character.PrimaryPart
			playsound(sound)
			for i,v in Character:GetDescendants() do
				if v:IsA("BasePart") and v.Transparency~=1 and colorhash[v.Name]==nil then
					table.insert(texturepayload,covertexture(script.Black,v))
				end
			end 
			local animpayload=ReplaceAnimations(script["Dark Form"],Character.Animate)
			FormData["Dark Form"].Data["Colors"]=payload
			FormData["Dark Form"].Data["Animations"]=animpayload
			FormData["Dark Form"].Data["Textures"]=texturepayload
			FormData["Dark Form"].Data["Particles"]=particlepayload
			FormData["Dark Form"].Data["Sounds"]={sound}
			end,
		[false]=function(Character)
			if FormData["Dark Form"].Data.Colors and #FormData["Dark Form"].Data.Colors>1 then
			for i,v in FormData["Dark Form"].Data.Colors do 
				ts:Create(v.object,imagetrans,{Color=v.Color}):Play()
			end	
			for i,v in FormData["Dark Form"].Data.Textures do 
				for t,o in v do 
				ts:Create(o,imagetrans,{Transparency=1}):Play()
				debris:AddItem(o,7)
				end
			end
			FormData["Dark Form"].Data.Textures={}
			for i,v in FormData["Dark Form"].Data.Sounds do 
					ts:Create(v,imagetrans,{Volume=0}):Play()
					debris:AddItem(v,8)
			end	
			FormData["Dark Form"].Data.Sounds={}
			for i,v in FormData["Dark Form"].Data.Particles do 
				v.Enabled=false 
				debris:AddItem(v,5)
				--v:Destroy()
			end
			
			FormData["Dark Form"].Data.Particles={}
			FormData["Dark Form"].Data.Colors={}
			local v =FormData["Dark Form"].Data.Animations-- do 
				for t,o in v.payload do 
					o.Parent=Character.Animate
				end
				for t,o in v.objects do 
					o.Parent=script["Dark Form"]
				end
			--end	
			FormData["Dark Form"].Data.Animations={}
		end 
		end,
		["Data"]={},
		Connections=function(Character)
			local a=Character.ChildAdded:Connect(function(v)
				if v:IsA("Tool") then
					Character.Humanoid:UnequipTools()
				end
			end)
			local b=Character.Humanoid:GetPropertyChangedSignal("Health"):Connect(function()
			if Character.Humanoid.Health<=1 then
				Character.Humanoid.Health=1				
				Forms.Toggle(Character,"Dark Form",true)
			elseif Character.Humanoid.Health>=Character.Humanoid.MaxHealth*.25 then
				Forms.Toggle(Character,"Dark Form",false)
			end
			end)
			return {a,b}
		end,
	},
}


function Forms.Toggle(Character,FormName,bool)
	local formobject=Form(Character,FormName)
	--if you want to turn it off is fine otherwise check conditions
	if bool==false or formobject.Value==true or FormData[FormName].Conditions==nil or CheckConditions(FormData[FormName].Conditions,Character) then
	if bool==nil then
	if formobject.Value==false then
		bool=true
		formobject.Value=true
	else 
		formobject.Value=false
		bool=false 	
	end
	elseif formobject.Value==bool then
		return 			
	end
	formobject.Value=bool
		FormData[FormName][bool](Character)
	end	
end
function Forms.GetUserData(Character)
	local Player=game.Players:GetPlayerFromCharacter(Character)
	local userId=Player.UserId
	local loadedTransformations = loadTransformations(userId)
	if loadedTransformations==nil or loadedTransformations=={} or loadedTransformations["Dark Form"]==nil then
		local payload={}
		print(FormData)
		for i,v in FormData do 
			print(i)
			print(v.initial)
			payload[i]=v.initial
		end
		print(payload)
		loadedTransformations=payload
		saveTransformations(userId, payload)
	end
	for i,v in FormData do 
		if loadedTransformations[i]==nil then
		print(i)
		print(v.initial)
		loadedTransformations[i]=v.initial
		end
	end
	print(loadedTransformations)
	local c=Instance.new("BoolValue")
	c.Name="Transformations"
	for i,v in loadedTransformations do 
		local d
		if typeof(v)=="boolean" then
		d=Instance.new("BoolValue")
		elseif typeof(v)=="number" then
		d=Instance.new("NumberValue")	
		elseif typeof(v)=="string" then
		d=Instance.new("StringValue")	
		end
		d.Value=v
		d.Name=i
		d.Parent=c
		d:GetPropertyChangedSignal("Value"):Connect(function()
		c.Value=true
		end)
	end
	c.Parent=Character
	local savedebounce=false
	c:GetPropertyChangedSignal("Value"):Connect(function()
		if c.Value==true then
		if savedebounce==true then
		repeat task.wait(1) until savedebounce==false or c.Value==false 	
		end
		savedebounce=true
		local payload={}
			for i,v in c:GetChildren() do 
				payload[v.Name]=v.Value
			end
		saveTransformations(userId, payload)
		c.Value=false	
		task.delay(30,function()
			savedebounce=false
		end)
		end
	end)
	return c
	--print("Data loaded Successfully and Save hook initialized")
end
function Forms.Initalize(Character,FormHash)
local connections={}
local userData=Forms.GetUserData(Character)
for t,o in userData:GetChildren() do
	local i=o.Name
	local d=o.Value
--for i,v in FormData do	
	if FormHash==nil or FormHash[i]~=nil then
	Form(Character,i)
	if d~=false then
	table.insert(connections,FormData[i].Connections(Character))
	else 
		o:GetPropertyChangedSignal("Value"):Connect(function()
			if o.Value==true then
			table.insert(connections,FormData[i].Connections(Character))	
			elseif o.Value==false then
				Forms.Toggle(Character,i,false)
			end
		end)
	end
	end
end

local function disconnect()
	for i,group in connections do
		for t,connection in group do 
			connection:Disconnect()
		end
	end
end
table.insert(connections,Character.AncestryChanged:Connect(function()
	if Character.Parent~=workspace then
		disconnect()
	end
end))


end


Forms.Initalize(Character)

image

image

image