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