Very gross class (help me clean it)

The title explains it all:

--!strict
--/ roblox services
local TweenService = game:GetService("TweenService")
local CollectionService = game:GetService("CollectionService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ReplicatedFirst = game:GetService("ReplicatedFirst")
local UserInputService = game:GetService("UserInputService")
local SoundService = game:GetService("SoundService")
local Players = game:GetService("Players")
--/ folders
local Source = ReplicatedStorage.Source
local Classes = Source.Classes
local Modules = Source.Modules
local Util = Modules.Util
local Util2 = ReplicatedFirst.Source.Util
local Packages = ReplicatedStorage.Packages
local Assets = ReplicatedStorage.Assets
--/ requires
local Customization = require(script.Customization) -- settings table
local Trove = require(Packages._Index["sleitnick_trove@1.1.0"]["trove"])
local DragToRotate = require(script.DragToRotateViewportFrame)
local ViewportModel = require(Modules.ViewportModel)
local PlaySound = require(Util.playSound)
local FreezePlayer = require(Util2.freezePlayer)
--/ class
local Interaction = {}
Interaction.__index = Interaction

local Interface = SoundService.Interface
local InspectGuiPreface = Assets.InspectGuiPreface
local ControlsGuiPreface = Assets.ControlsGuiPreface
local NotesPreface = Assets.NotesPreface
local DialogueGuiPreface = Assets.DialogueGuiPreface

local Mouse = Players.LocalPlayer:GetMouse()
local PlayerGui = Players.LocalPlayer:FindFirstChild("PlayerGui")
local PreviousModel: Model?

local Interacting = false
local CurrentObject = nil

local function createHighlight(Adornee: Instance?): Highlight
	local Highlight = Instance.new("Highlight")
	Highlight.FillTransparency = 1
	Highlight.OutlineTransparency = 0
	Highlight.DepthMode = Enum.HighlightDepthMode.Occluded
	Highlight.Parent = Adornee
	Highlight.Adornee = Adornee
	
	return Highlight
end

export type ClassType = typeof( setmetatable({} :: {
	Connections: Trove.ClassType,
	InteractionConnections: Trove.ClassType,
	RaycastParams: RaycastParams,
	Distance: number,
	Character: any,
	Controls: any,
	Blacklist: any,
	Instances: any,
	ControlGui: any,
}, Interaction) )


function Interaction.new(): ClassType
	local self = {
		Connections = Trove.new(),
		InteractionConnections = Trove.new(),
		Controls = {
			UserInputType = {};
			KeyCode = {};
		},
		Blacklist = {},
		Instances = {},
		RaycastParams = RaycastParams.new(),
		Distance = 10,
		
		Character = nil,
		ControlGui = nil,
	};
	
	setmetatable(self, Interaction)
	
	return self
end

function Interaction.Enable(self: ClassType): ()
	local TaggedInstances = CollectionService:GetTagged(Customization.Tag)

	for _, instance in pairs(TaggedInstances) do
		if not instance:IsA("Instance") then
			continue
		end

		self.Instances[instance.Name] = instance
	end
	
	self:_setupCharacterAddedFunction()
	-- params
	table.insert(self.Blacklist,self.Character)
	self.RaycastParams.FilterDescendantsInstances = self.Blacklist
	self.RaycastParams.FilterType = Enum.RaycastFilterType.Exclude
	-- connections
	--/ moving mouse detection
	self.Connections:Connect(Mouse.Move, function() 
		self:MouseMove()
	end)
	--/ touch moved = dragging mobile 
	self.Connections:Connect(UserInputService.TouchMoved, function() 
		self:TouchMove()
	end)
end

function Interaction._setupCharacterAddedFunction(self: ClassType)
	self.Connections:Connect(Players.LocalPlayer.CharacterAdded, function(character: Model) 
		if not self.Character then
			self.Character = character
		end
	end)

	if Players.LocalPlayer then
		if not self.Character then
			self.Character = Players.LocalPlayer.Character:: Model
		end
	end
end

function Interaction.MouseMove(self: ClassType): () -- pc
	if not self.Character then
		return
	end
	
	if Interacting then
		if PreviousModel then
			if CurrentObject and CurrentObject ~= PreviousModel then
				return
			end
			
			if PreviousModel:FindFirstChildOfClass("Highlight") then
				local Highlight: any = PreviousModel:FindFirstChildOfClass("Highlight") 
				Highlight:Destroy()

				self:DisconnectInteractions()
			end

			if not PreviousModel:FindFirstChildOfClass("Highlight") then
				return
			end

			local Highlight: any = PreviousModel:FindFirstChildOfClass("Highlight") 
			Highlight:Destroy()

			self:DisconnectInteractions()
		end


		return
	end
	
	local Origin = self.Character["Head"].Position
	local MousePosition = Mouse.Hit.Position
	local Direction = (MousePosition - Origin).Unit * self.Distance
	
	local Result = game.Workspace:Raycast(Origin, Direction, self.RaycastParams)
	
	if Result then
		CurrentObject = Result.Instance
	
		if PreviousModel and CurrentObject ~= PreviousModel then
			if PreviousModel:FindFirstChildOfClass("Highlight") then
				local Highlight: any = PreviousModel:FindFirstChildOfClass("Highlight") 
				Highlight:Destroy()
				
				self:DisconnectInteractions()
			end
		end
			
		if not CollectionService:HasTag(CurrentObject, Customization.Tag) then
			if CollectionService:HasTag(CurrentObject.Parent, Customization.Tag) then
				CurrentObject = Result.Instance.Parent
			else
				return
			end
		end
		
		if not CurrentObject:FindFirstChildOfClass("Highlight") then
			PlaySound("rbxassetid://3199281218")
			createHighlight(CurrentObject)
			
			self:InteractConnections() -- handles interactive elements modules
		end
		
		PreviousModel = CurrentObject
	end
	
	if not Result and PreviousModel then
		if not PreviousModel:FindFirstChildOfClass("Highlight") then
			return
		end
		
		local Highlight: any = PreviousModel:FindFirstChildOfClass("Highlight") 
		Highlight:Destroy()
		
		self:DisconnectInteractions()
	end
end

function Interaction.TouchMove(self: ClassType): () -- mobile
	
end

--[[
 Interactive Functions:
 
 These functions can be used inside the second module, to make it easier to replicate effects
 Purpose is for efficieny instead of copying the same code inside a different module
--]]

function Interaction.CreateNote(content: string, signature: string, Image: string)
	if PlayerGui:FindFirstChild("Note") then
		return
	end
	
	Interacting = true
	
	local Connection = Trove.new()
	local NotesUI = NotesPreface:Clone()
	
	local NoteOpen = Interface.NoteOpen
	local NoteClose = Interface.NoteClose
	
	local BackgroundFrame = NotesUI.BackgroundFrame
	local Paper = BackgroundFrame.Paper
	local Text = Paper.Content
	local Signature = Paper.Signature
	
	if Image then
		Paper.Image = Image
	end
	
	Text.Text = content
	Signature.Text = signature
	
	--TODO: Freeze Player + Show Note + Any button to exit.
	FreezePlayer(true)
	PlaySound(NoteOpen)
	
	NotesUI.Name = "Note"
	NotesUI.Parent = PlayerGui
	
	Connection:Connect(UserInputService.InputBegan, function(input: InputObject, gameProcessedEvent: boolean) 
		if input.UserInputType ~= Enum.UserInputType.MouseButton1 then
			FreezePlayer(false)
			PlaySound(NoteClose)
			
			Interacting = false
			NotesUI:Destroy()
			Connection:Destroy()
		end
	end)
end

function Interaction.Inspect(Model: Model): ()
	if not Model then
		return
	end
	
	if PlayerGui:FindFirstChild("Inspect") then
		return
	end
	
	Interacting = true
	
	PlaySound(Interface.InspectOpen)
	FreezePlayer(true)
	
	local Connection = Trove.new()
	local InspectGui = InspectGuiPreface:Clone()
	local ViewportFrame = InspectGui.BackgroundFrame.ViewportFrame
	
	local VPFCamera = Instance.new("Camera")
	VPFCamera.FieldOfView = 70
	VPFCamera.Parent = ViewportFrame
	
	InspectGui.Parent = Players.LocalPlayer.PlayerGui
	InspectGui.Name = "Inspect"
	local vpfModel = ViewportModel.new(ViewportFrame, VPFCamera)
	local dtrViewportFrame = DragToRotate.New(ViewportFrame)
	dtrViewportFrame.MouseMode = "Default"
	
	local Model = Model:Clone()
	Model.Parent = ViewportFrame.WorldModel
	
	local cf, size = Model:GetBoundingBox()

	dtrViewportFrame:SetModel(Model)
	vpfModel:SetModel(Model)

	local distance = vpfModel:GetFitDistance(cf.Position * 1.2) 
	
	VPFCamera.CFrame = CFrame.new(cf.Position) * CFrame.new(0, 0, distance)

	ViewportFrame.InputBegan:Connect(function(inputObject)
		if inputObject.UserInputType == Enum.UserInputType.MouseButton1 then
			local inputObjectChangedC: any
			dtrViewportFrame:BeginDragging()

			inputObjectChangedC = inputObject.Changed:Connect(function()
				if inputObject.UserInputState == Enum.UserInputState.End then
					inputObjectChangedC:Disconnect()
					inputObjectChangedC = nil
					
					dtrViewportFrame:StopDragging()
				end
			end)
		end
	end)
	
	Connection:Connect(UserInputService.InputBegan, function(input: InputObject, gameProcessedEvent: boolean) 
		if input.UserInputType ~= Enum.UserInputType.MouseButton1 then
			FreezePlayer(false)
			PlaySound(Interface.InspectClose)
			
			Interacting = false
			dtrViewportFrame:StopDragging()
			InspectGui:Destroy()
			Connection:Destroy()
		end
	end)
end

function Interaction.Zoom(Input: Enum.KeyCode, FOVIN: number)
	local InputEnded: RBXScriptConnection
	
	local tweenZoomIn = TweenService:Create(
		workspace.CurrentCamera, 
		TweenInfo.new(0.3), 
		{FieldOfView = FOVIN})
	
	local tweenZoomOut = TweenService:Create(
		workspace.CurrentCamera, 
		TweenInfo.new(0.3), 
		{FieldOfView = 70})
	
	
	Interface.ZoomIn:Play()
	tweenZoomIn:Play()
	
	InputEnded = UserInputService.InputEnded:Connect(function(input: InputObject, gameProcessedEvent: boolean)  
		if input.KeyCode == Input then
			if tweenZoomIn.PlaybackState == Enum.PlaybackState.Playing then
				tweenZoomIn:Pause()
			end

			tweenZoomOut:Play()
			InputEnded:Disconnect()
		end
	end)
end

function Interaction.Dialogue(YieldTime)
	if not CurrentObject then
		return
	end
	
	if PlayerGui:FindFirstChild("Dialogue") then
		warn("Existing Dialogue found.")
		return
	end
	
	local Dialogue = CurrentObject:GetAttribute("Dialogue"):: string
	
	if not Dialogue then
		warn("Dialogue attribute not found")
		return 
	end
	
	local DialogueGui = DialogueGuiPreface:Clone()
	DialogueGui.Name = "Dialogue"
	DialogueGui.Parent = PlayerGui
	
	local SizingFrame = DialogueGui.SizingFrame
	local DialogueFrame = SizingFrame.DialogueFrame
	local Text = DialogueFrame.Text
	local Shadow = DialogueFrame.ImageLabel
	
	Text.TextTransparency = 1
	Shadow.ImageTransparency = 1
	
	local tweenShadowIn = TweenService:Create(
		Shadow,
		TweenInfo.new(0.3),
		{ImageTransparency = 1}
	)
	
	local tweenShadowOut = TweenService:Create(
		Shadow,
		TweenInfo.new(0.3),
		{ImageTransparency = 0}
	)
	
	local tweenTextOut = TweenService:Create(
		Text,
		TweenInfo.new(0.3),
		{TextTransparency = 0}
	)

	local tweenTextIn = TweenService:Create(
		Text,
		TweenInfo.new(0.3),
		{TextTransparency = 1}
	)
	
	Text.Text = Dialogue
	
	tweenTextOut:Play()
	tweenShadowOut:Play()
	
	PlaySound(Interface.DialogueOpen)
	
	task.spawn(function()
		tweenTextOut.Completed:Connect(function()
			task.wait(YieldTime)
			
			tweenTextIn:Play()
			tweenShadowIn:Play()
		end)
	end)
	
	tweenTextIn.Completed:Connect(function()
		DialogueGui:Destroy()
	end)
end


function Interaction.ShowControls(self: ClassType): ()
	if #self.Controls.UserInputType == 0 and #self.Controls.KeyCode == 0  then
		warn(CurrentObject.Name, "doesn't have controls.")
		return
	end
	
	if not self.ControlGui then
		local ControlsGui = ControlsGuiPreface:Clone()
		self.ControlGui = ControlsGui
	end
	
	local SizingFrame = self.ControlGui.SizingFrame
	local KeyCodeFrame = SizingFrame.KeyCode
	local UserInputTypeFrame = SizingFrame.UserInputType
	local Images = SizingFrame.ControlImages
	
	for _, control in pairs(self.Controls.KeyCode) do
		for _, image in pairs(Images:GetChildren()) do
			if image.Name == control then
				image:Clone().Parent = KeyCodeFrame
			end
		end
	end
	
	for _, control in pairs(self.Controls.UserInputType) do
		for _, image in pairs(Images:GetChildren()) do
			if image.Name == control then
				image:Clone().Parent = UserInputTypeFrame
			end
		end
	end
	
	self.ControlGui.Parent = Players.LocalPlayer.PlayerGui
end

function Interaction.CleanControls(self: ClassType): ()
	table.clear(self.Controls.KeyCode)
	table.clear(self.Controls.UserInputType)
	
	if not self.ControlGui then -- nothing to clean
		return
	end
	
	local SizingFrame = self.ControlGui.SizingFrame
	local T1 = SizingFrame.KeyCode:GetChildren()
	local T2 = SizingFrame.UserInputType:GetChildren()
	
	table.move(T2, 1, #T2, #T1 + 1, T1) -- merges two tables into t1
	
	for _, instance: Instance in pairs(T1) do
		if not instance:IsA("ImageLabel") then
			continue
		end
		
		instance:Destroy()
	end
end

function Interaction.InteractConnections(self: ClassType): ()
	if not CurrentObject:FindFirstChild("Interact") then
		warn("Couldn't find interaction module inside of:", CurrentObject.Name)
		return
	end
	
	local ModuleScript = CurrentObject.Interact
	local Module = require(ModuleScript)
	
	local UITenums = Enum.UserInputType:GetEnumItems()
	local KCenums = Enum.KeyCode:GetEnumItems()
	
	local function CheckEnums(string): string
		for _, v in pairs(KCenums) do
			if v.Name == string then
				return "KeyCode"
			end
		end
		
		for _, v in pairs(UITenums) do
			if v.Name == string then
				return "UserInputType"
			end
		end
		
		return "None"
	end
	
	for fnName, fnCode in pairs(Module) do
		if type(Module[fnName]) ~= "function" then
			continue
		end
		
		local EnumType = CheckEnums(fnName)
		
		if EnumType == "UserInputType" and table.find(UITenums, Enum.UserInputType[fnName]) then
			table.insert(self.Controls.UserInputType, fnName) 
		end

		if EnumType == "KeyCode" and table.find(KCenums, Enum.KeyCode[fnName]) then
			table.insert(self.Controls.KeyCode, fnName)
		end
	end
	
	self.InteractionConnections:Connect(UserInputService.InputBegan, function(input: InputObject, gameProcssedEvent: boolean)
		local func = Module[input.KeyCode.Name] or Module[input.UserInputType.Name]

		if (func ~= nil) then
			func()
		end
	end)
	
	self:ShowControls()
end

function Interaction.DisconnectInteractions(self: ClassType)
	self.InteractionConnections:Clean()
	self:CleanControls()
end

function Interaction.Disconnect(self: ClassType)
	self.Connections:Clean()
	self.InteractionConnections:Clean()
	
	self:CleanControls()
end


return Interaction

A fun challenge for perfectionists, see how far you can go with optimization!

600+ lines

2 Likes