commit 891781366d2ef40eaf43fd6a2605ba620feb0d27 Author: paladin Date: Wed Jun 10 02:53:40 2026 +0100 Copy salient and essential scripts and models from Plane Building Roblox diff --git a/models/archive.rbxm b/models/archive.rbxm new file mode 100644 index 0000000..489648d Binary files /dev/null and b/models/archive.rbxm differ diff --git a/models/main_gui.rbxm b/models/main_gui.rbxm new file mode 100644 index 0000000..c96f7bc Binary files /dev/null and b/models/main_gui.rbxm differ diff --git a/models/parts.rbxm b/models/parts.rbxm new file mode 100644 index 0000000..93dde81 Binary files /dev/null and b/models/parts.rbxm differ diff --git a/models/topbar_gui.rbxm b/models/topbar_gui.rbxm new file mode 100644 index 0000000..78e3b32 Binary files /dev/null and b/models/topbar_gui.rbxm differ diff --git a/pb.rbxl b/pb.rbxl new file mode 100644 index 0000000..b23b098 Binary files /dev/null and b/pb.rbxl differ diff --git a/scripts/LoadingScreen b/scripts/LoadingScreen new file mode 100644 index 0000000..75afc24 --- /dev/null +++ b/scripts/LoadingScreen @@ -0,0 +1,31 @@ +local tween_service = game:GetService("TweenService") +local run_service = game:GetService("RunService") +local player = game.Players.LocalPlayer +local playerGui = player.PlayerGui + +local loadingGui = script:WaitForChild("LoadingGui") +loadingGui.Parent = playerGui + +local offset = 6 +if run_service:IsStudio() then offset = 2 end + +local prop_name = { + TextLabel = "TextTransparency", + ImageLabel = "ImageTransparency", + Frame = "BackgroundTransparency" +} +wait(1) +for _,item in pairs(loadingGui:GetChildren()) do + local property = prop_name[item.ClassName] + if not item:IsA("Frame") then + local tween = tween_service:Create(item,TweenInfo.new(2,Enum.EasingStyle.Quad,Enum.EasingDirection.Out,0,false,0),{[property] = 0}) + tween:Play() + tween.Completed:Once(function() + local tween2 = tween_service:Create(item,TweenInfo.new(1,Enum.EasingStyle.Quad,Enum.EasingDirection.Out,0,false,1),{[property] = 1}) + tween2:Play() + end) + else + local tween2 = tween_service:Create(item,TweenInfo.new(1,Enum.EasingStyle.Quad,Enum.EasingDirection.Out,0,false,offset),{[property] = 1}) + tween2:Play() + end +end \ No newline at end of file diff --git a/scripts/ReadyModule b/scripts/ReadyModule new file mode 100644 index 0000000..77f13ca --- /dev/null +++ b/scripts/ReadyModule @@ -0,0 +1,4 @@ +local ready +if game:GetService("RunService"):IsClient() then ready = game.ReplicatedFirst.ReadyClient else ready = game.ReplicatedFirst.ReadyServer end +if not ready.Value then ready.Changed:Wait() end +return function() end \ No newline at end of file diff --git a/scripts/WaterVisibility b/scripts/WaterVisibility new file mode 100644 index 0000000..290a52d --- /dev/null +++ b/scripts/WaterVisibility @@ -0,0 +1,9 @@ +local run = game:GetService("RunService") +run.RenderStepped:Connect(function() + game.Lighting.Blur.Enabled = false + game.Lighting.WaterCorrection.Enabled = false + if game.Workspace.CurrentCamera.CFrame.Position.Y < 0 then + game.Lighting.Blur.Enabled = true + game.Lighting.WaterCorrection.Enabled = true + end +end) \ No newline at end of file diff --git a/scripts/events.txt b/scripts/events.txt new file mode 100644 index 0000000..ff6424b --- /dev/null +++ b/scripts/events.txt @@ -0,0 +1,9 @@ +# Vehicle // ?! +Command +Create +Delete +Notification +Pick +Place +Save +Saved \ No newline at end of file diff --git a/scripts/global/BuildingGlobals.lua b/scripts/global/BuildingGlobals.lua new file mode 100644 index 0000000..5e5be68 --- /dev/null +++ b/scripts/global/BuildingGlobals.lua @@ -0,0 +1,123 @@ +Methods = require(_G.Modules.MethodModule) +Placement = require(_G.Modules.PlacementModule) +Statistics = {} +Configuration = Statistics +function GetValue(part,name,default) + local instance = part:FindFirstChild(name) + if not instance then return default + else return instance.Value end +end +function GetInstance(part,name) + return part:FindFirstChild(name) or error("Missing sub-instance "..name..":"..part:GetFullName(),3) +end + +function Print(instance,...) + local text = table.concat({string.format("@%.2f:",tick() - _G.Begin),...}," ") + if _G.PrintUpdate then _G.PrintUpdate(instance,text) end + if _G.IsServer then _G.Remotes.DebugUpdate:FireAllClients(instance,...) end +end + +_G.Rules = {} +_G.Rules.Sandbox = { + GenerationVersion = "4", + GenerationFollowPlayer = true, + AutoSpawnIdlers = true, + NeedsExperience = false, + NeedsInventory = false, + NeedsMaterial = false, + NeedsCurrency = false, +} +_G.Rules.Survival = { + GenerationVersion = "5", + GenerationFollowPlayer = true, + AutoSpawnIdlers = false, + AutoSpawnPirates = true, + AutoSpawnFreighters = true, + NeedsExperience = true, + NeedsInventory = true, + NeedsMaterial = true, + NeedsCurrency = false, +} +_G.Rules.War = { + GenerationVersion = "4", + GenerationFollowPlayer = false, + AutoSpawnIdlers = false, + AutoSpawnPirates = false, + AutoSpawnFreighters = false, + NeedsExperience = false, + NeedsInventory = true, + NeedsMaterial = true, + NeedsCurrency = true +} +_G.RuleMode = "Sandbox" + +_G.Notify = function(title,content) + game.ReplicatedStorage.Remotes.Notification:FireAllClients(title,content) +end + +_G.SaveCompression = true +_G.SaveFormat = "4" + +_G.Changelog = [[ +DUCK5_0: +Figuring out scripting with Lua and Rust. +Bevy setup. + +NEL0: +Parser, interpreter and LI standard library complete. + +PB3: +More computer fixes, I forget. +Added steam engines. +Adding balloons. + +DCT1: +Scrapped old buildings for 'slice trees' + +DCT0: +Implemented working terrain engine. +Loads of sorcery: +Poly terrain (Mesh or WedgePart) +Buildings Beta + +PB2: +Fixed ropes and rods scaling. +Fixed ropes and rods. +Fixed motors. +Made some fixes to CreateToolScript and PaintToolScript. +Made DebugWings deactivate correctly. +Did various fixes on ProgramModule. +Added meshes. +Tried to fix propellers. +Fixed wheel resizing. +Fixed rod bug in save system. +Fixed the anchor block bug. + +PB1: +Fixed fatal materials paint bug. +Fixed model resizing bug. +Fixed disconnectors bug. +Fixed guns bug. +Fixed notifications looking stupid. +Fixed spring edit updates. +Fixed a poly sticks save bug. +Protected various objects from error. +Fixed microcontroller dragger bug. +Fixed microcontroller deletion bug. +Fixed poly snapping. +Added a changelog. + +PB0: +Object system 'Agnostic Ownership' overhaul. +Added Microcontrollers. +Added new engine models. +Aero system implements viscosity (gliding). +Made various changes to object properties. +Poly now supports up to 9 nodes. +Poly now supports thickness changes. +Many unrecorded QOL changes. (like 200 more lines xD) +]] + +_G.Union(_G,getfenv()) -- Combine _G with this module + +return getfenv() \ No newline at end of file diff --git a/scripts/global/DefaultGlobals.lua b/scripts/global/DefaultGlobals.lua new file mode 100644 index 0000000..494aed7 --- /dev/null +++ b/scripts/global/DefaultGlobals.lua @@ -0,0 +1,161 @@ +function Union(this,other) -- Combine two tables + for index,item in pairs(other) do + this[index] = item + end + return this +end + +function Default(this,other) + for index,item in pairs(other) do + if not this[index] then this[index] = item end + end +end + +local _string = {["string"] = true} +local _number = {["number"] = true} +local _table = {["table"] = true} +local _boolean = {["boolean"] = true} +local type = type +IsString = function(value) return not not _string[type(value)] end +IsNumber = function(value) return not not _number[type(value)] end +IsTable = function(value) return not not _table[type(value)] end +IsBoolean = function(value) return not not _boolean[type(value)] end + +Modules = game.ReplicatedFirst:WaitForChild("Modules") +Storage = game.ReplicatedStorage +Assets = Storage:WaitForChild("Assets") +Sounds = Assets:WaitForChild("Sounds") +Events = game.ReplicatedFirst:WaitForChild("Events") +Remotes = Storage:WaitForChild("Remotes") +Debris = game:GetService("Debris") +Tags = game:GetService("CollectionService") +Tween = game:GetService("TweenService") +Run = game:GetService("RunService") +Input = game:GetService("UserInputService") +Http = game:GetService("HttpService") +IsServer = Run:IsServer() +IsStudio = Run:IsStudio() +IsClient = not IsServer +if IsClient then Player = game.Players.LocalPlayer end + +Begin = tick() + +if IsServer then + Data = game:GetService("DataStoreService") +else + Player = game.Players.LocalPlayer + UserId = Player.UserId + Gui = game.Players.LocalPlayer.PlayerGui +end + + +function Iterate(array) + local i = 0 + local n = #array + return function() + i = i + 1 + if i <= n then return array[i] end + end +end + +function WaitFor(tab,item,timeout) + local t = 0 + timeout = timeout or 60 + repeat wait() t = t + 1 if t > timeout then error("Timeout for "..item,3) end until tab[item] + return tab[item] +end + +Errors = {} + +function Error(stuff) + local traceback = debug.traceback("\nTraceback:",3) + table.insert(Errors,tostring(stuff)..traceback) + warn(stuff,traceback) +end + +function Protect(func) + return function(...) + local success,result = xpcall(func,function(err) + Error(err) + end,...) + return result + end +end + +function Protected(func,...) + local success,result = xpcall(func,function(err) + Error(err) + end,...) + if success then return result end +end + +String = tostring +Append = table.insert +ClassOf = getmetatable +Percent = function(num) + if not num then error("Number is nil.",2) end + local val = math.ceil(num*3) + local colour = "31" + if val > 2 then + colour = "32" + elseif val > 1 then + colour = "33" + end + local txt = "\27["..colour.."m"..String(math.floor(num*100)).."%".."\27[0m" + return txt +end + +Unimplemented = function() + error("Method unimplemented",2) +end + +Notify = function(...) + Events.Notification:Fire(...) +end + +Class = function(properties) + local template = {} -- Creates a metatable + template.templates = {} + template.Descends = function(other) + return template.templates[getmetatable(other)] + end + template.__index = {} + if properties.Inherits then -- Combine the template of the superclass + template.__index = Union({},properties.Inherits.__index or {}) + template.__tostring = properties.Inherits.__tostring + properties.Inherits.templates[template] = true + template.super = properties.Inherits + properties.Inherits = nil + end + properties.Class = template + template.templates[template] = true -- Add this to the table of + if properties.Construct then + template.constructor = properties.Construct or function(_,...) end + properties.Construct = nil + end + for index,item in pairs(properties) do -- The remaining items in properties are data + template.__index[index] = item -- So all constants and methods go here + end + template.create = template.create or function(this,...) -- Call template to make this + local new = {} -- Create a new instance table + template.constructor(new,...) -- Construct it + return setmetatable(new,template) -- Return the newly made instance + end + setmetatable(template,{ + __call = template.create, + }) + template.__tostring = properties.String or template.__tostring + return template +end + +Union(_G,getfenv()) -- Combine _G with this module + +require(script.Parent.BuildingGlobals) + +if IsServer then + game.ReplicatedFirst.ReadyServer.Value = true +else + game.ReplicatedFirst.ReadyClient.Value = true +end + +return _G \ No newline at end of file diff --git a/scripts/global/GlobalsSetupClient.lua b/scripts/global/GlobalsSetupClient.lua new file mode 100644 index 0000000..b8fade6 --- /dev/null +++ b/scripts/global/GlobalsSetupClient.lua @@ -0,0 +1 @@ +require(script.Parent.DefaultGlobals) \ No newline at end of file diff --git a/scripts/global/GlobalsSetupServer.lua b/scripts/global/GlobalsSetupServer.lua new file mode 100644 index 0000000..b8fade6 --- /dev/null +++ b/scripts/global/GlobalsSetupServer.lua @@ -0,0 +1 @@ +require(script.Parent.DefaultGlobals) \ No newline at end of file diff --git a/scripts/gui/ExpansionScript.lua b/scripts/gui/ExpansionScript.lua new file mode 100644 index 0000000..4dc956a --- /dev/null +++ b/scripts/gui/ExpansionScript.lua @@ -0,0 +1,26 @@ +local defaultPosition = script.Parent.Saves.Position +local defaultSize = script.Parent.Saves.Size +local expandedPosition = UDim2.new(0.5,-defaultSize.X.Offset,0,defaultPosition.Y.Offset) +local expandedSize = UDim2.new(0,defaultSize.X.Offset*2,0,defaultSize.Y.Offset) +local toggle = false +local input = game:GetService("UserInputService") +local function expansion() + toggle = not toggle + local newSize = defaultSize + local newPosition = defaultPosition + if toggle then + newSize = expandedSize + newPosition = expandedPosition + end + script.Parent.Saves:TweenSizeAndPosition(newSize,newPosition,Enum.EasingDirection.InOut,Enum.EasingStyle.Quad,0.5,true) +end +input.InputEnded:Connect(function(input,irrelevant) + if not irrelevant and script.Parent.Enabled then + if input.UserInputType == Enum.UserInputType.Keyboard then + if input.KeyCode == Enum.KeyCode.E then + expansion() + end + end + end +end) +script.Parent.MinimizeButton.Activated:Connect(expansion) \ No newline at end of file diff --git a/scripts/gui/NotificationScript.lua b/scripts/gui/NotificationScript.lua new file mode 100644 index 0000000..14e8410 --- /dev/null +++ b/scripts/gui/NotificationScript.lua @@ -0,0 +1,43 @@ +local template = script.Notification +local function Popup(title,message) + local notification = template:Clone() + notification.TitleLabel.Text = title + notification.ContentLabel.Text = message + notification.Parent = script.Parent.Parent.ScreenGui + notification.Position = UDim2.new(0.5,0,0.5,0) + notification.Size = UDim2.new(0,400,0,0) + notification.AnchorPoint = Vector2.one*0.5 + notification.UIDragDetector.Enabled = true + notification.TitleLabel.DestroyButton.Activated:Connect(function() + notification:Destroy() + end) +end +local function Notification(title,message) + local notification = template:Clone() + notification.TitleLabel.Text = title or "Untitled" + notification.ContentLabel.Text = tostring(message) + notification.Parent = script.Parent.Left + notification.Size = UDim2.new(0,0,0,0) + notification.TitleLabel.DestroyButton.Visible = false + local tapped = function() + Popup(title,message) + end + notification.TouchTap:Connect(tapped) + notification.InputEnded:Connect(function(input) + if input.UserInputType == Enum.UserInputType.MouseButton1 then tapped() end + end) + notification:TweenSize(UDim2.new(0,210,0,0),Enum.EasingDirection.InOut,Enum.EasingStyle.Quad,0.5,true,function() + wait(3) + notification.ContentLabel.Size = UDim2.new(0,notification.ContentLabel.AbsoluteSize.X,0,notification.ContentLabel.AbsoluteSize.Y) + notification:TweenSize(UDim2.new(0,0,0,0),Enum.EasingDirection.InOut,Enum.EasingStyle.Quad,0.5,true,function() + notification:Destroy() + end) + end) +end +require(game.ReplicatedFirst.ReadyModule) +_G.Remotes.Notification.OnClientEvent:Connect(function(title,message) + Notification(title,message) +end) +_G.Events.Notification.Event:Connect(function(title,message) + Notification(title,message) +end) \ No newline at end of file diff --git a/scripts/gui/OptionScript.lua b/scripts/gui/OptionScript.lua new file mode 100644 index 0000000..bf6a3e7 --- /dev/null +++ b/scripts/gui/OptionScript.lua @@ -0,0 +1,159 @@ +require(game.ReplicatedFirst.ReadyModule) +local optionFrame = script.Parent:WaitForChild("OptionFrame") +local options = optionFrame:WaitForChild("Options") +function registerToggle(name,callback) + local button = options:WaitForChild(name) + local value = button.TextButton.Text == "On" + button.TextButton.MouseButton1Click:Connect(function() + value = not value + local new = "On" + if not value then + new = "Off" + end + button.TextButton.Text = new + callback(value) + _G.Events.Notification:Fire(name,new) + end) +end +local function registerSlider(name,callback) + local slider = options:WaitForChild(name) + local drag = slider:WaitForChild("Slider"):WaitForChild("Drag") + local etch = slider:WaitForChild("Slider"):WaitForChild("Etch") + local hold + local position + local event + local function changed(object) + if hold then + local delta = (object.Position - hold).X + local final = math.clamp((delta/(etch.AbsoluteSize.X))+position,0,1) + drag.Position = UDim2.new(final,0,0,0) + callback(final) + else + event:Disconnect() + end + end + drag.InputBegan:Connect(function(object) + if object.UserInputType == Enum.UserInputType.MouseButton1 or object.UserInputType == Enum.UserInputType.Touch then + hold = object.Position + position = drag.Position.X.Scale + event = _G.Input.InputChanged:Connect(changed) + end + end) + drag.InputEnded:Connect(function(object) + if object.UserInputType == Enum.UserInputType.MouseButton1 or object.UserInputType == Enum.UserInputType.Touch then + hold = nil + position = nil + end + end) +end +local collaborateButton = options:WaitForChild("CollaborateButton") +collaborateButton.MouseButton1Click:Connect(function() + _G.Remotes.Collaborate:InvokeServer(nil,nil) +end) +registerSlider("Air",function(value) + game.Workspace.CompensateDensity.Value = value * 2 +end) +registerToggle("Destruction",function(value) + game.Workspace.Destruction.Value = value +end) +registerToggle("Combat",function(value) + game.Workspace.Combat.Value = value + if value then + _G.Remotes.Combat:InvokeServer() + end +end) +registerToggle("Debug",function(value) + game.Workspace.DebugWings.Value = value +end) +registerToggle("Stats",function(value) + _G.StatisticsFrame.Visible = value +end) +registerSlider("Dead",function(value) + local val = value > 0.5 + game.Lighting.ColorCorrection.Enabled = val + game.Lighting.ColorGrading.Enabled = val +end) +local defaultDensity = 0.262 +local defaultBlur = 24 +local changeFog = function(value) + game.Lighting.Atmosphere.Density = defaultDensity*value + game.Lighting.Blur.Size = defaultBlur*value +end +local changeTime = function(value) + game.Lighting.Time.Value = value * 24 +end +registerSlider("Fog",changeFog) +registerSlider("Time",changeTime) +--if _G.Statistics.fog_enabled then changeFog() options:WaitForChild("Fog").Text = "Off" end +local minimized = true +local toggle = _G:WaitFor("OptionsButton") +local extended = optionFrame.Position +local retracted = UDim2.new(UDim.new(0,-optionFrame.AbsoluteSize.X),extended.Y) +optionFrame.Position = retracted +optionFrame.Visible = true +local function toggleMenu() + print("toggle") + minimized = not minimized + optionFrame.Visible = not minimized + --[[local new + if minimized then + new = retracted + else + new = extended + end + optionFrame:TweenPosition(new,Enum.EasingDirection.InOut,Enum.EasingStyle.Quad,0.4,true)]] +end +toggle.Activated:Connect(toggleMenu) +local input = game:GetService("UserInputService") +input.InputBegan:Connect(function(input,irrelevant) + if not irrelevant and input.KeyCode == Enum.KeyCode.Backquote then + if input:IsModifierKeyDown(Enum.ModifierKey.Shift) then + minimized = true + optionFrame.Visible = false + optionFrame.Options.Command.CommandBox:ReleaseFocus() + else + minimized = false + optionFrame.Visible = true + wait() optionFrame.Options.Command.CommandBox:CaptureFocus() + end + end +end) + +--game.StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Chat,false) +local messageFrame = script.Parent:WaitForChild("MessageFrame") +local messageBox = messageFrame:WaitForChild("TextBox") +local messageContainer = messageFrame:WaitForChild("ScrollingFrame") +_G.Remotes.Chat.OnClientEvent:Connect(function(title,content,color) + local message = messageContainer.Template.Message:Clone() + message.Parent = messageContainer + message.Title.Text = title + message.Content.Text = content + message.Title.TextColor3 = color + message.Visible = true + messageContainer.CanvasPosition = Vector2.new(0, messageContainer.CanvasSize.Y.Offset - messageContainer.AbsoluteSize.Y) +end) +_G.Input.InputBegan:Connect(function(input,irrelevant) + if not irrelevant and input.KeyCode == Enum.KeyCode.Slash then + messageBox:CaptureFocus() + end +end) +_G:WaitFor("ChatHideButton").Activated:Connect(function() + messageFrame.Visible = not messageFrame.Visible +end) +messageBox.FocusLost:Connect(function(enterPressed) + if enterPressed then + local message = messageBox.Text + :gsub("\n","
") + :gsub("
","
") + :gsub("(.-)","%1") + :gsub("(.-)","%1") + :gsub("(.-)","%1") + :gsub("(.-)","%1") + :gsub("(.-)","%1") + :gsub("_(.-)_","%1") + :gsub("*(.-)*","%1") + if #message == 0 then return end + _G.Remotes.Chat:FireServer(message) + messageBox.Text = "" + end +end) \ No newline at end of file diff --git a/scripts/gui/PlacementScript.lua b/scripts/gui/PlacementScript.lua new file mode 100644 index 0000000..755ac56 --- /dev/null +++ b/scripts/gui/PlacementScript.lua @@ -0,0 +1,2 @@ +require(game.ReplicatedFirst.ReadyModule) +require(_G.Modules.PlacementModule).init(script.Parent) \ No newline at end of file diff --git a/scripts/gui/SaveScriptNew.lua b/scripts/gui/SaveScriptNew.lua new file mode 100644 index 0000000..e360a62 --- /dev/null +++ b/scripts/gui/SaveScriptNew.lua @@ -0,0 +1,2 @@ +require(game.ReplicatedFirst.ReadyModule) +require(_G.Modules.SaveModule).InitUI(script) \ No newline at end of file diff --git a/scripts/gui/ShopScript.lua b/scripts/gui/ShopScript.lua new file mode 100644 index 0000000..3c24e89 --- /dev/null +++ b/scripts/gui/ShopScript.lua @@ -0,0 +1,183 @@ +require(game.ReplicatedFirst.ReadyModule) +local shopGui = script.Parent +_G.ShopGui = shopGui +local shopFrame = shopGui.Items +local shopMaterial = shopGui.CashLabel +local shopBackButton = shopGui.BackButton +local shopSearch = shopGui.Search + +-- Parts +local partsStore = _G.Storage:WaitForChild("Parts") +local namedFrames = {} +local directNamedFrames = {} +local folders = {} +local currentFolder = partsStore +local currentTerm = "" + +local function createItemFrame(item) + local defaultFrame = shopFrame.Grid.ItemFrame + defaultFrame.Visible = false + + -- Create ViewportFrame and put a camera in + local itemFrame = defaultFrame:Clone() + itemFrame.Parent = shopFrame + itemFrame.ItemLabel.Text = item:GetAttribute("Name") or item.Name + itemFrame.Visible = true + if not item:HasTag("ConnectorObject") then itemFrame.PolyLabel:Destroy() end + itemFrame.NewLabel:Destroy() + itemFrame.Name = item:GetAttribute("Name") or item.Name + + local viewportFrame = itemFrame.ViewportFrame + + local camera = Instance.new("Camera") + camera.Parent = viewportFrame + + viewportFrame.CurrentCamera = camera + + item = item:Clone() + item.Parent = viewportFrame + item:PivotTo(item:GetAttribute("Rotation") or CFrame.identity) + local distance + local cameraRotation = 45 + if item:IsA("BasePart") then + distance = item.Size.Magnitude + elseif item:IsA("Model") then + if not item.PrimaryPart then + warn(item:GetFullName().." has no PrimaryPart, please set one!") + item.PrimaryPart = item:FindFirstChildWhichIsA("BasePart") + end + distance = item:GetExtentsSize().Magnitude + end + + local function rotateCamera() + camera.CFrame = CFrame.fromEulerAnglesYXZ(-0.8,math.rad(cameraRotation),0) + camera.CFrame = camera.CFrame + camera.CFrame.LookVector * -(distance/1.2) + end + rotateCamera() + + local mouseInside = false + viewportFrame.MouseEnter:Connect(function() + mouseInside = true + viewportFrame.BackgroundColor3 = Color3.new(0.75,0.75,0.75) + while wait() and mouseInside do + cameraRotation = cameraRotation + 0.33 + rotateCamera() + end + end) + viewportFrame.MouseLeave:Connect(function() mouseInside = false viewportFrame.BackgroundColor3 = Color3.new(1,1,1) end) + + return itemFrame +end + +local function clearItems() + for _,item in pairs(shopFrame:GetChildren()) do + if item:IsA("Frame") then + item.Parent = nil -- Now is this a good idea or not... + end + end +end + +local f = string.find +local l = string.lower +local function loadSearch(term) + clearItems() + wait() + term = l(term) + for _,info in pairs(namedFrames) do + if f(l(info[1]),term,1,true) then -- A crude search if anything + info[2].Parent = shopFrame + end + end + for _,info in pairs(directNamedFrames) do + if f(l(info[1]),term,1,true) then -- A crude search if anything + info[2].Parent = shopFrame + end + end +end + +shopSearch.FocusLost:Connect(function(enter, inputObject) + if enter then + -- add a quick enter + end +end) + +local function loadFolder(folder) + currentFolder = folder + clearItems() + wait() + for _,frame in pairs(folders[folder]) do + frame.Parent = shopFrame + end +end + +shopSearch.Changed:Connect(function() + if currentTerm == shopSearch.Text then return end + currentTerm = shopSearch.Text + if #shopSearch.Text > 0 then + loadSearch(shopSearch.Text) + else + loadFolder(currentFolder) + end +end) + +local existing = {} +local t = 200 +local function registerItem(item) + if existing[item] then return end + existing[item] = true + if item:HasTag("DoNotPlace") then return end + local isFolder = item:IsA("Folder") + local isPart = item.Parent:IsA("Folder") and (item:IsA("Model") or item:IsA("BasePart")) + if isFolder or isPart then + local model = item + if isFolder then + local smallestOrder = t+1 + local smallestItem + for _,child in pairs(item:GetDescendants()) do + if not (child.Parent:IsA("Folder") and (child:IsA("BasePart") or child:IsA("Model"))) then continue end + local order = child:GetAttribute("Order") or t + if order < smallestOrder then smallestOrder = order smallestItem = child end + end + model = smallestItem + end + if not model then warn(item:GetFullName().." has no children.") return end + local frame = createItemFrame(model) + frame.LayoutOrder = item:GetAttribute("Order") or t + folders[item.Parent] = folders[item.Parent] or {} -- Make sure the folder exists + table.insert(folders[item.Parent],frame) + if isPart then + if item:GetAttribute("Name") then table.insert(namedFrames,{item:GetAttribute("Name"),frame}) end + table.insert(directNamedFrames,{item.Name,frame}) + frame.InputEnded:Connect(function(inputObject) + if inputObject.UserInputType == Enum.UserInputType.MouseButton1 or inputObject.UserInputType == Enum.UserInputType.Touch then + _G.Events.Pick:Fire(item) + if item.Parent:IsA("Folder") and item.Parent:HasTag("AutoBack") then + loadFolder(item.Parent.Parent) + end + end + end) + else + frame.ItemLabel.Text = item.Name + frame.InputEnded:Connect(function(inputObject) + if inputObject.UserInputType == Enum.UserInputType.MouseButton1 or inputObject.UserInputType == Enum.UserInputType.Touch then + loadFolder(item) + end + end) + end + end +end + +local function loadStore() + for _,item in pairs(partsStore:GetDescendants()) do + registerItem(item) + end + loadFolder(currentFolder) +end + +shopBackButton.MouseButton1Click:Connect(function() + local parent = currentFolder.Parent + if currentFolder == partsStore then parent = partsStore end + loadFolder(parent) +end) + +loadStore() \ No newline at end of file diff --git a/scripts/gui/ToolScript.lua b/scripts/gui/ToolScript.lua new file mode 100644 index 0000000..b22fc71 --- /dev/null +++ b/scripts/gui/ToolScript.lua @@ -0,0 +1,182 @@ +local StarterGui = game:GetService("StarterGui") +StarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Backpack, false) + +local frame = script.Parent +frame.Visible = true + +local input = game:GetService("UserInputService") + +local player = game.Players.LocalPlayer +local backpack = player.Backpack + +local stroke = script:WaitForChild("UIStroke") + +local currentTool = nil + +local tools = {} + +local frames = {} + +local function isToolRegistered(tool) return tools[tool] end + +local function getToolFrame(tool) return frames[tool] end + +local function stockTool(tool) + tool.Parent = backpack + stroke.Parent = script + stroke.Transparency = 1 + frames[tool].Deselected:Fire() +end + +local index = 0 + +local function setTool(tool) + if currentTool then stockTool(currentTool) end -- If there is already a tool equipped, dequip it + if currentTool == tool then currentTool = nil return end -- If this tool is already equipped, remove it and stop + + if tonumber(tool.Name) then index = tonumber(tool.Name) end + frames[tool].Selected:Fire() + tool.Parent = player.Character -- Equip the new tool + stroke.Parent = frames[tool] + stroke.Transparency = 0.2 + currentTool = tool +end + +local nameToInteger = { + ["One"] = 1, + ["Two"] = 2, + ["Three"] = 3, + ["Four"] = 4, + ["Five"] = 5, + ["Six"] = 6, + ["Seven"] = 7, + ["Eight"] = 8, + ["Nine"] = 9 +} + +input.InputBegan:Connect(function(input,irrelevant) + if not irrelevant then + local tools = 0 + for _,item in pairs(frame.ScrollingFrame:GetChildren()) do + if item:FindFirstChild("ToolReference") then tools = tools + 1 end + end + local inputted = false + if input.UserInputType == Enum.UserInputType.Keyboard then + local number = nameToInteger[input.KeyCode.Name] + if number then inputted = true index = number end + elseif input.KeyCode == Enum.KeyCode.ButtonR1 then + inputted = true + index = index + 1 + elseif input.KeyCode == Enum.KeyCode.ButtonL1 then + inputted = true + index = index - 1 + end + index = math.clamp(index,0,tools+1) + if not inputted then return end + local toolFrame = frame.ScrollingFrame:FindFirstChild(tostring(index)) + if toolFrame then + setTool(toolFrame.ToolReference.Value) + else + if currentTool then stockTool(currentTool) currentTool = nil end + end + end +end) + +local function createToolFrame(tool) + local toolFrame = frame.ScrollingFrame.Template.Tool:Clone() + toolFrame.ToolReference.Value = tool + local layout = tool:WaitForChild("LayoutOrderValue",1) + if layout then layout = layout.Value + else layout = #frames end + toolFrame.LayoutOrder = layout + frames[tool] = toolFrame + tools[tool] = tool + local clone = tool:Clone() + for _,child in pairs(clone:GetChildren()) do + if child:IsA("Script") then + child:Destroy() + end + end + toolFrame.Parent = frame.ScrollingFrame + toolFrame.Visible = true + toolFrame.Name = tostring(toolFrame.LayoutOrder) + local viewport = toolFrame.ViewportFrame + viewport.Name = clone.Name + clone.Parent = viewport + local camera = Instance.new("Camera") + viewport.CurrentCamera = camera + camera.Parent = viewport + local label = toolFrame.TextLabel + label.Text = tool.Name + local cframe,size = clone:GetBoundingBox() + + local cameraRotation = 45 + local distance = size.Magnitude + + local function rotateCamera() -- To constantly spin the tool view + camera.CFrame = CFrame.new(cframe.Position) * CFrame.fromEulerAnglesYXZ(-0.3,math.rad(cameraRotation),0) + camera.CFrame = camera.CFrame + camera.CFrame.LookVector * -(distance/1.2) + end + + rotateCamera() -- To initialise the camera CFrame once + + local mouseInside = 0 -- Rotate camera while the mouse is inside the tool + local mouseEnter = function(no_back) + mouseInside = mouseInside + 1 + if not no_back then toolFrame.BackgroundColor3 = Color3.new(0.75,0.75,0.75) end + if mouseInside == 1 then while wait() and mouseInside > 0 do + cameraRotation = cameraRotation + 1.5 * mouseInside + rotateCamera() + end end + end + local mouseLeave = function(no_back) + mouseInside = mouseInside - 1 + if not no_back then toolFrame.BackgroundColor3 = Color3.new(1,1,1) end + end + viewport.MouseEnter:Connect(mouseEnter) + viewport.MouseLeave:Connect(mouseLeave) + toolFrame.Selected.Event:Connect(mouseEnter) + toolFrame.Deselected.Event:Connect(mouseLeave) + return toolFrame +end + +local function unregisterTool(tool) + --print(tool:GetFullName()) + if currentTool == tool then stroke.Parent = script currentTool = nil end + if frames[tool] then frames[tool]:Destroy() end + frames[tool] = nil + tools[tool] = nil +end + +local function registerTool(tool) + if isToolRegistered(tool) then return end -- Don't register an existing tool + + tool.AncestryChanged:Connect(function(tool,parent) -- Event for when parent changes + if not (parent == player.Character or parent == backpack) then -- If the tool is not a parent of the character or backpack, it is dropped + unregisterTool(tool) + end + end) + + createToolFrame(tool) + + -- todo: save me + frames[tool].InputEnded:Connect(function(inputObject) -- When the mouse button or touch is clicked (XBOX + VR CONTROL NEEDED) + if inputObject.UserInputType == Enum.UserInputType.MouseButton1 or inputObject.UserInputType == Enum.UserInputType.Touch then + setTool(tool) + end + end) + --[[frames[tool].TouchTap:Connect(function() + setTool(tool) + end)]] +end + +game.Players.LocalPlayer.ChildAdded:Connect(function(child) + if currentTool then unregisterTool(currentTool) end + if child:IsA("Backpack") then backpack = child end + backpack.ChildAdded:Connect(registerTool) + backpack.ChildRemoved:Connect(registerTool) +end) + +for _,tool in pairs(backpack:GetChildren()) do + registerTool(tool) +end \ No newline at end of file diff --git a/scripts/gui/note.txt b/scripts/gui/note.txt new file mode 100644 index 0000000..4ef311d --- /dev/null +++ b/scripts/gui/note.txt @@ -0,0 +1 @@ +Most things are not relative to the original hierarchy. I'm just dumping everything here. \ No newline at end of file diff --git a/scripts/modules/BulletModule.lua b/scripts/modules/BulletModule.lua new file mode 100644 index 0000000..9a39aeb --- /dev/null +++ b/scripts/modules/BulletModule.lua @@ -0,0 +1,91 @@ +if _G.IsClient then + local module = {} + + function module.fire(barrel) + spawn(function() + local bullet = _G.Remotes.Shoot:InvokeServer(barrel) + bullet = game.Workspace:WaitForChild(bullet) + if not bullet then return end + bullet.CFrame = barrel.CFrame * CFrame.Angles(0,math.rad(-90),0) + bullet.Velocity = bullet.CFrame.LookVector * 1000 + --bullet.BodyVelocity.Velocity = bullet.Velocity + end) + end + + return module +elseif _G.IsServer then + -- SERVER-SIDE + function touched(touched,team,player,bullet,barrel) + if not touched or not touched.Parent then return end + local humanoid = touched.Parent:FindFirstChildWhichIsA("Humanoid") + if humanoid then + if humanoid.Parent:FindFirstChild("Team") and humanoid.Parent.Team.Value == game.Teams.Loon then + humanoid.Health = humanoid.Health - 25 + bullet:Destroy() + end + end + local object + local part + if touched.Parent:IsA("Model") and touched.Parent:HasTag("Combat") then + object = touched.Parent + part = object.PrimaryPart + else + part = touched + object = touched + end + if object:HasTag("Combat") then + if not object:HasTag("P_"..player.UserId) then + if not object:GetAttribute("Health") then + local max = part.Size.Magnitude/1.5 + if object:IsA("Model") then max = object:GetExtentsSize().Magnitude/1.5 end + object:SetAttribute("Health",max) + object:SetAttribute("MaxHealth",max) + end + local health = object:GetAttribute("Health") + local max = object:GetAttribute("MaxHealth") + health = health - 3 + --part.Transparency = 1-(health/max) + if barrel.BlastPressure.Value ~= -1 then + local explosion = Instance.new("Explosion") + explosion.BlastPressure = barrel.BlastPressure.Value + explosion.DestroyJointRadiusPercent = barrel.Destruction.Value + explosion.Parent = part + explosion.Position = part.Position + end + + object:SetAttribute("Health",health) + if health < 0 then + _G.Methods.break_part(object,player) + object.Parent = game.Workspace.Hidden -- stop busted mechanics please + end + bullet:Destroy() + end + end + end + + function fire(barrel,team,player) + if not barrel then return end + local bullet = _G.Storage[barrel.Parent.Name.."Bullet"]:Clone() + delay(1.8,function() if bullet and bullet.Parent then bullet:Destroy() end end) + bullet.CFrame = CFrame.new(0,1000000000,0) + bullet.Parent = game.Workspace + local connection = bullet.Touched:Connect(function(part) + touched(part,team,player,bullet,barrel) + end) + bullet.CFrame = barrel.CFrame * CFrame.Angles(0,math.rad(-90),0) + bullet.Velocity = bullet.CFrame.LookVector * 1000 + bullet.Anchored = false + bullet:SetAttribute("Team",team.Name) + if player then + bullet:SetNetworkOwner(player) --[[delay(0,function() bullet:SetNetworkOwnershipAuto() end)]] + bullet:SetAttribute("Owner",player.UserId) + end + bullet:FindFirstChildWhichIsA("Sound"):Play() + bullet.Name = bullet.Name .. tostring(math.random(1,1000000)) + return bullet.Name + end + + _G.Remotes.Shoot.OnServerInvoke = function(player,barrel) return fire(barrel,player.Team,player) end + + return getfenv() +end \ No newline at end of file diff --git a/scripts/modules/CharacterModule.lua b/scripts/modules/CharacterModule.lua new file mode 100644 index 0000000..51d8cb3 --- /dev/null +++ b/scripts/modules/CharacterModule.lua @@ -0,0 +1,153 @@ +motors = {} +modes = {} +cframes = {} +targets = {} +c0s = {} +c1s = {} +torso = nil +head = nil +model = nil +humanoid = nil +running = false +cameraOffset = Vector3.new(0,0,0) +run = game:GetService("RunService") +input = game:GetService("UserInputService") +motorsEnabled = true +function getMotor(name) + motors[name] = torso:WaitForChild(name) + cframes[name] = CFrame.new(0,0,0) -- need to change this to a {} for each motor, not globals + targets[name] = CFrame.new(0,0,0) + c0s[name] = motors[name].C0 + c1s[name] = motors[name].C1 + return motors[name] +end +function check(...) + for _,item in pairs({...}) do + if not item or not item.Parent then return false end + end + return true +end +local alpha = 0.1 * (1/60) +function setArm(name,delta) + if not delta then delta = 1/60 end + if not check(model,head,torso,motors[name]) or motorsEnabled or humanoid.SeatPart then return end + local origin = (game.Workspace.CurrentCamera.CFrame.Rotation + head.Position) + if modes[name] ~= 1 then cframes[name] = origin:ToObjectSpace(motors[name].Part1.CFrame) * CFrame.Angles(-1.57,0,0) end + modes[name] = 1 + cframes[name] = cframes[name]:Lerp(targets[name],alpha/delta) + motors[name].Part1.CFrame = origin:ToWorldSpace(cframes[name] * CFrame.Angles(1.57,0,0)) +end +function restArm(name,delta) + if not delta then delta = 1/60 end + if not check(model,head,torso,motors[name]) or motorsEnabled or humanoid.SeatPart then return end + local origin = (torso.CFrame) + if modes[name] ~= 2 then cframes[name] = origin:ToObjectSpace(motors[name].Part1.CFrame) * CFrame.Angles(-1.57,0,0) end + modes[name] = 2 + cframes[name] = cframes[name]:Lerp(targets[name],alpha/delta) + motors[name].Part1.CFrame = origin:ToWorldSpace(cframes[name] * CFrame.Angles(1.57,0,0)) +end +function resetMotors() + for name,motor in pairs(motors) do + if motor and motor.Parent then + motor.C0 = c0s[name] + motor.C1 = c1s[name] + end + end +end +function rotateCharacter() + if not model or not model.Parent or not model.PrimaryPart or not humanoid then return end + local state = humanoid:GetState() + local statetype = Enum.HumanoidStateType + if state == statetype.Ragdoll or state == statetype.FallingDown or state == statetype.GettingUp then return end + model.HumanoidRootPart.CFrame = + CFrame.new(model.HumanoidRootPart.Position, + model.HumanoidRootPart.Position + + Vector3.new( + game.Workspace.CurrentCamera.CFrame.LookVector.X,0, + game.Workspace.CurrentCamera.CFrame.LookVector.Z)) +end +function lookArm(name,position) + +end +function rotateHead() + if not motors["Neck"] or not torso then return end + local target = torso.CFrame:ToObjectSpace(game.Workspace.Camera.CFrame).LookVector + motors["Neck"].C0 = CFrame.new(0, 1, 0, -1, 0, 0, 0, 0, 1, 0, 1, -0) * CFrame.Angles(math.acos(target.Y)+math.rad(-105),0,math.acos(target.X)+math.rad(-90)) +end +function disableMotors() + if not motorsEnabled then return end + motorsEnabled = false + getMotor("Left Shoulder").Enabled = false + local leftArm = motors["Left Shoulder"].Part1 + leftArm.CanCollide = false + leftArm.Anchored = true + getMotor("Right Shoulder").Enabled = false + local rightArm = motors["Right Shoulder"].Part1 + rightArm.CanCollide = false + rightArm.Anchored = true +end +function enableMotors() + if motorsEnabled then return end + motorsEnabled = true + motors["Left Shoulder"].Enabled = true + local leftArm = motors["Left Shoulder"].Part1 + leftArm.CanCollide = true + leftArm.Anchored = false + motors["Right Shoulder"].Enabled = true + local rightArm = motors["Right Shoulder"].Part1 + rightArm.CanCollide = true + rightArm.Anchored = false +end +function setRunning(value) + if value then + running = true + humanoid.WalkSpeed = 30 + else + running = false + humanoid.WalkSpeed = 15 + end +end +run:BindToRenderStep("CharacterMotorReset",1,function() resetMotors() rotateHead() + game.Players.LocalPlayer.CameraMode = Enum.CameraMode.Classic + if humanoid then humanoid.CameraOffset = cameraOffset end cameraOffset = Vector3.new(0,0,0) +end) +run:BindToRenderStep("CharacterHeadRotate",Enum.RenderPriority.Last.Value+1,function() + rotateHead() +end) + +function init(character) + torso = character:WaitForChild("Torso") + head = character:WaitForChild("Head") + humanoid = character:WaitForChild("Humanoid") + humanoid.BreakJointsOnDeath = false + motors = {} + model = character + enableMotors() + getMotor("Neck").Enabled = true + getMotor("Left Shoulder").Enabled = true + getMotor("Right Shoulder").Enabled = true + spawn(function() while wait(.1) do + local event = _G.Remotes.MotorMoveClient + event:FireServer(motors["Neck"],motors["Neck"].C0,motors["Neck"].C1) + event:FireServer(motors["Left Shoulder"],motors["Left Shoulder"].C0,motors["Left Shoulder"].C1) + event:FireServer(motors["Right Shoulder"],motors["Right Shoulder"].C0,motors["Right Shoulder"].C1) + end end) + input.InputBegan:Connect(function(object,irrelevant) + if not irrelevant then + if object.KeyCode == Enum.KeyCode.LeftShift or object.KeyCode == Enum.KeyCode.ButtonL1 then + humanoid.WalkSpeed = 30 + running = true + end + end + end) + input.InputEnded:Connect(function(object,irrelevant) + if not irrelevant then + if object.KeyCode == Enum.KeyCode.LeftShift or object.KeyCode == Enum.KeyCode.ButtonL1 then + humanoid.WalkSpeed = 15 + running = false + end + end + end) + game.Workspace.CurrentCamera.CameraSubject = humanoid +end +return getfenv() \ No newline at end of file diff --git a/scripts/modules/CommandsModule.lua b/scripts/modules/CommandsModule.lua new file mode 100644 index 0000000..61f35d1 --- /dev/null +++ b/scripts/modules/CommandsModule.lua @@ -0,0 +1,54 @@ +require(game.ReplicatedFirst.ReadyModule)("CommandsModule") + +local help = "" +local commands = {} + +function invoke(input,player) + local found = false + for index,command in ipairs(commands) do + local result = table.pack(input:match(command.pattern)) + if result[1] then + if player then + table.insert(result,1,player) + end + found = true + command.callback(table.unpack(result)) + end + end + return found +end + +function new(pattern,documentation,callback) + help = help .. documentation .. "\n" + table.insert(commands,{ + pattern = pattern, + documentation = documentation, + callback = callback + }) + if _G.IsServer then _G.Storage:WaitForChild("CommandsHelp").Value = help end +end + +_G.Events.Command.Event:Connect(function(command) + local cFound = invoke(command) + local sFound = _G.Remotes.Command:InvokeServer(command) + if not cFound and not sFound then _G.Events.Notification:Fire("Command Not Found",[[ +If you're struggling to type in a command, note that they use pattern matching. +"" means you should literally type "12345678", no letters or dots. +Check your spelling or type 'help' for some commands. + ]]) end +end) + +if _G.IsServer then + _G.Remotes.Command.OnServerInvoke = function(player,command) + return invoke(command,player) + end +end + +local notification +new("help","help: displays this message",function() + local final = "Client:\n"..help + if _G.IsClient then final = final.."\nServer:\n".._G.Storage.CommandsHelp.Value end + _G.Events.Notification:Fire("Command Help",final) +end) + +return getfenv() \ No newline at end of file diff --git a/scripts/modules/DraggingModule.lua b/scripts/modules/DraggingModule.lua new file mode 100644 index 0000000..741a3da --- /dev/null +++ b/scripts/modules/DraggingModule.lua @@ -0,0 +1,41 @@ +local module = {} + +local function dragger(object,callback,root) + local finger + object.InputBegan:Connect(function(input) + if finger then return end + if root then + local objects = game.Players.LocalPlayer.PlayerGui:GetGuiObjectsAtPosition(input.Position.X, input.Position.Y) + if #objects == 0 or objects[1] == nil then return end -- Roblox what the hell? + while not objects[1]:IsDescendantOf(root) do table.remove(objects,1) end + if objects[1] ~= object then return end + end + if input.UserInputType == Enum.UserInputType.Touch then finger = input end + if input.UserInputType == Enum.UserInputType.MouseButton1 then finger = true end + if not finger then return end + local options = callback(Vector2.new(input.Position.X,input.Position.Y)) + local function drag(input) + if options.drag then options.drag(Vector2.new(input.Position.X,input.Position.Y)) end + end + drag(input) + local changed + changed = _G.Input.InputChanged:Connect(function(input) + if input.UserInputType == Enum.UserInputType.MouseMovement or input == finger then + drag(input) + end + end) + local ended + ended = _G.Input.InputEnded:Connect(function(input) + if input.UserInputType == Enum.UserInputType.MouseButton1 or input == finger then + drag(input) + if options.ended then options.ended(Vector2.new(input.Position.X,input.Position.Y)) end + changed:Disconnect() + ended:Disconnect() + finger = nil + end + end) + end) +end + + +return {dragger = dragger} diff --git a/scripts/modules/FloatModule.lua b/scripts/modules/FloatModule.lua new file mode 100644 index 0000000..c18ff89 --- /dev/null +++ b/scripts/modules/FloatModule.lua @@ -0,0 +1,127 @@ +-- this code LOOKS scary, but its actually pretty simple and fast +local items = {} +local is_server = not game.Players.LocalPlayer +local userid +if not is_server then + userid = game.Players.LocalPlayer.UserId +end + +-- to stop allocating new variables each time (threading doesn't work anyways) +local position +local velocity +local delta1 +local delta2 +local delta +local difference +local vp +local this_root + +local function round(n) + return math.round(n*1000)/1000 +end + +local common_delta = 1/120 +local common_drag_resistance = 0.967 +local common_float_resistance = 0.8 +local float_terminal = 1 +local decay = 10 +local complement = -11.5 +local idecay = 1/decay +local anti_gravity = Vector3.new(0,196,0) +local floating_velocity = 10 +local run = game:GetService("RunService") +local global_drag_resistance = 0 +local global_float_resistance = 0 +local global_delta = 0 + +run.PreSimulation:Connect(function(delta) + global_float_resistance = math.pow(common_float_resistance,delta/common_delta)-1 + global_drag_resistance = math.pow(common_drag_resistance,delta/common_delta)-1 + global_delta = delta +end) +local water_height = -4 +local decay_complement = -water_height+decay+complement +local template_instance = _G.Storage.Idol +local impulse = template_instance.ApplyImpulseAtPosition +local angular_impulse = template_instance.ApplyAngularImpulse +local network_owner_check_success, network_owner +local get_network_owner_real = template_instance.GetNetworkOwner +local get_network_owner = function(instance) + network_owner_check_success,network_owner = pcall(function() get_network_owner_real(instance) end) + return ((not network_owner_check_success) or network_owner) +end +local root_part = template_instance.GetRootPart +local new_vec3 = Vector3.new +local counter = 0 + +local root_reduction = {} + +local function reduce_root_part(root) + root.Velocity = root.Velocity * 0.99 + root.AssemblyAngularVelocity = root.AssemblyAngularVelocity * 0.99 +end +local math_clamp = math.clamp +local local_disconnect_all = true +local function register_part(instance) + if not instance:IsA("BasePart") then return end + if not instance.Parent then return end + local size = instance.Size + local volume = size.X*size.Y*size.Z + local mass = instance.Mass + local event + local operate = true + local splash = instance:FindFirstChild("Splash") + if splash and not splash:IsA("ParticleEmitter") then splash = nil end + if is_server and get_network_owner(instance) then return end + if instance.Massless then return end + instance.AncestryChanged:Connect(function() if not instance.Parent then operate = false event:Disconnect() end end) + --instance.Destroying:Connect(function() print("disconnect") event:Disconnect() end) + event = run.PreSimulation:Connect(function(delta) + --if not operate then event:Disconnect() end + + if is_server and get_network_owner(instance) then wait(0.2) return end + vp = instance.Position + position = vp.Y+decay_complement + if position < 0 then + --if splash then splash.Enabled = true splash.Rate = math_clamp(10*instance.AssemblyLinearVelocity.Magnitude,0,100) end -- PLEASE OPTIMISE + position = math.abs(position*idecay) + --instance.Transparency = 0.5 + if position > 1 then position = 1 end + velocity = instance.Velocity + --local angular = instance.AssemblyAngularVelocity -- implement later... + difference = velocity*global_drag_resistance + new_vec3(0,velocity.Y-floating_velocity,0)*global_float_resistance + impulse(instance,difference*volume*position+anti_gravity*delta,vp) + else + --if splash then splash.Enabled = false end -- PLEASE OPTIMISE + --velocity = instance.Velocity + wait(0.1) -- to stop checking too many times in air (saving frames) + end + end) + return event +end +if is_server then + game.Workspace.Objects.DescendantAdded:Connect(function(instance) + local owner = instance:GetAttribute("NetworkOwner") + if not owner and is_server then + --register_part(instance) + for _,child in pairs(instance:GetChildren()) do + --register_part(child) + end + end + end) +else + game.Workspace.Objects.DescendantAdded:Connect(function(instance) + local owner = instance:GetAttribute("NetworkOwner") + if owner == userid then + --register_part(instance) + for _,child in pairs(instance:GetChildren()) do + --register_part(child) + end + end + end) +end + + +local module = {} +module.register_part = register_part +return module \ No newline at end of file diff --git a/scripts/modules/GamemodeModule.lua b/scripts/modules/GamemodeModule.lua new file mode 100644 index 0000000..2edcb32 --- /dev/null +++ b/scripts/modules/GamemodeModule.lua @@ -0,0 +1,36 @@ +function init(this,mode) + if mode == "Survival" then + this.Parent.Triggered:Connect(function(player) + for _,tool in pairs(player.Character:GetChildren()) do + if tool:IsA("Tool") then + tool:Destroy() + end + end + for _,tool in pairs(player.Backpack:GetChildren()) do + if tool:IsA("Tool") then + tool:Destroy() + end + end + for _,tool in pairs(_G.Storage.Tools:GetChildren()) do + tool:Clone().Parent = player.Backpack + end + player.Character:SetPrimaryPartCFrame(game.Workspace.Heaven.AngelSpawn.CFrame+Vector3.new(0,4,0)) + end) + end + if mode == "Heaven" then + this.Parent.Triggered:Connect(function(player) + for _,tool in pairs(player.Character:GetChildren()) do + if tool:IsA("Tool") then + tool:Destroy() + end + end + for _,tool in pairs(player.Backpack:GetChildren()) do + if tool:IsA("Tool") then + tool:Destroy() + end + end + player.Character:SetPrimaryPartCFrame(game.Workspace.GoonSpawn.CFrame+Vector3.new(0,4,0)) + end) + end +end +return getfenv() \ No newline at end of file diff --git a/scripts/modules/GunModule.lua b/scripts/modules/GunModule.lua new file mode 100644 index 0000000..d73201e --- /dev/null +++ b/scripts/modules/GunModule.lua @@ -0,0 +1,200 @@ +c = require(_G.Modules.CharacterModule) +l = "Left Shoulder" +r = "Right Shoulder" + +local lastShot = tick() +local input = game:GetService("UserInputService") +local bulletModule = nil +if script.Parent:FindFirstChild("BulletModule") then bulletModule = require(_G.Modules.BulletModule) end +function init(gun) + local fov = 60 + local returnTable = {} + local la = {} -- states for the arm in different animations + local ra = {} + returnTable.la = la + returnTable.ra = ra + returnTable.maxRounds = 10 + returnTable.shouldRest = true + returnTable.clock = 0.1 + returnTable.rounds = returnTable.maxRounds + local aiming = false + local shootEvent + local cameraLast = game.Workspace.CurrentCamera.CFrame.Rotation + + ra.aiming = CFrame.new(0,-1,-2) + ra.idling = CFrame.new(1,-1,-2) * CFrame.Angles(0,0,0.5) + ra.recoil = CFrame.new(0,0,1.5) + ra.sheath = CFrame.new(1.5,-4,0)*CFrame.Angles(-1.57,0,0) + ra.reload = {} + ra.reload.beforeSmashMagazine = CFrame.new(1,-0.5,-2)*CFrame.Angles(0.8,0.5,0) + ra.reload.smashingMagazine = CFrame.new(0.7,-0.9,-2)*CFrame.Angles(0.4,0.3,0) + ra.reload.beforeLoadingMagazine = CFrame.new(1,-0.5,-2)*CFrame.Angles(0.8,-0.5,0) + ra.reload.loadingMagazine = CFrame.new(1,-0.5,-2)*CFrame.Angles(0.8,-0.5,0) + ra.reload.beforePullBack = CFrame.new(2.2,-1,-2.5)*CFrame.Angles(-0.3,1.2,0.1) + ra.reload.pullBack = CFrame.new(1.7,-1,-2.5)*CFrame.Angles(-0.3,1.2,0.1) + + la.idling = CFrame.new(-1.5,0,0)*CFrame.Angles(-1.57,0,0) + la.resting = CFrame.new(-1.5,0,0)*CFrame.Angles(-1.57,0,0) + la.aiming = CFrame.new(-0.5,-1.3,-2) * CFrame.Angles(0.3,-1,0) + la.recoil = CFrame.new(0.5,0,0.75) + la.sheath = CFrame.new(-1.5,-1.3,0)*CFrame.Angles(-1.57,0,0) + la.reload = {} + la.reload.beforeSmashMagazine = CFrame.new(-1.1,-1.5,0)*CFrame.Angles(-1.57,0,0) + la.reload.smashingMagazine = CFrame.new(-1.1,-1.5,0)*CFrame.Angles(-1.57,0,0) + la.reload.beforeLoadingMagazine = CFrame.new(0.3,-1.1,-2.5) * CFrame.Angles(0.4,-1,0) + la.reload.loadingMagazine = CFrame.new(0.6,-0.7,-2)*CFrame.Angles(0.6,-1,0) + la.reload.beforePullBack = CFrame.new(-1,-1,-2.6)*CFrame.Angles(0.4,0,0) + la.reload.pullBack = CFrame.new(0.5,-1,-2.4)*CFrame.Angles(0.4,0,0) + + for name,cframe in pairs(la.reload) do + la.reload[name] = cframe + Vector3.new(-0.4,0,0) + end + for name,cframe in pairs(ra.reload) do + ra.reload[name] = cframe + Vector3.new(-0.4,0,0) + end + + gun.Equipped:Connect(function() + if c.humanoid.SeatPart then return end + + local rc = {} -- state for the arm + local lc = {} + + local gc = {} -- state for gun pully thing + + c.disableMotors() + + rc.offset = CFrame.new(0,0,0) + rc.target = ra.idling + lc.offset = CFrame.new(0,0,0) + lc.target = la.resting + rc.janky = CFrame.new() + lc.janky = CFrame.new() + if not returnTable.shouldRest then lc.target = la.idling end + gc.target = CFrame.new(0,0,0) + c.cframes[r] = ra.sheath c.setArm(r) + if not returnTable.shouldRest then c.setArm(l) c.cframes[l] = la.sheath c.setArm(l) + else c.restArm(l) c.cframes[l] = la.resting c.restArm(l) end + + c.run:UnbindFromRenderStep("GunMove") + c.run:UnbindFromRenderStep("Transparency") + + local modes = {["idle"]=1,["reload"]=function() + local scale = game.Workspace.Scale.Value + rc.target = ra.reload.beforeSmashMagazine + lc.target = la.reload.beforeSmashMagazine + wait(0.3*scale) + gun.MagOut:Play() + rc.target = ra.reload.smashingMagazine + lc.target = la.reload.smashingMagazine + wait(0.4*scale) + rc.target = ra.reload.beforeLoadingMagazine + lc.target = la.reload.beforeLoadingMagazine + wait(0.5*scale) + rc.target = ra.reload.loadingMagazine + lc.target = la.reload.loadingMagazine + gun.MagIn:Play() + wait(0.3*scale) + rc.target = ra.reload.beforePullBack + lc.target = la.reload.beforePullBack + wait(0.4*scale) + gun.Reload:Play() + if gun:FindFirstChild("Slide") then gc.target = gun.Slide.Value end + rc.target = ra.reload.pullBack + lc.target = la.reload.pullBack + wait(0.3*scale) + gc.target = CFrame.new(0,0,0) + end,} + local mode = modes.idle + local reloading = false + + if shootEvent then shootEvent:Disconnect() end + shootEvent = gun.Activated:Connect(function() + if not reloading and not c.running then + if returnTable.rounds == 0 and returnTable.maxRounds > 0 then + reloading = true + modes.reload() + reloading = false + returnTable.rounds = returnTable.maxRounds + return + end + if tick()-lastShot < returnTable.clock then return end + gun.Fired:Play() + lastShot = tick() + returnTable.rounds-=1 + local rot = Vector3.new(0,0,math.random(-10,10)/10) + rc.offset = ra.recoil * CFrame.Angles(rot.x,rot.y,rot.z) + if aiming or not returnTable.shouldRest then lc.offset = la.recoil end + if gun.Gun:FindFirstChild("GunShot") then + gun.Gun.GunShot.ParticleEmitter.Transparency = NumberSequence.new(0) + end + if gun:FindFirstChild("Slide") then gc.target = gun.Slide.Value end + if bulletModule and gun.Gun:FindFirstChild("Barrel") then bulletModule.fire(gun.Gun.Barrel) end + wait() + if gun.Gun:FindFirstChild("GunShot") then + gun.Gun.GunShot.ParticleEmitter.Transparency = NumberSequence.new(1) + end + rc.offset = CFrame.new(0,0,0) + lc.offset = CFrame.new(0,0,0) + wait(returnTable.clock/2) + gc.target = CFrame.new(0,0,0) + end + end) + c.run:BindToRenderStep("GunMove",Enum.RenderPriority.Last.Value,function(delta) + if gun.Parent ~= c.model then c.enableMotors() return end + --game.Players.LocalPlayer.CameraMode = Enum.CameraMode.LockFirstPerson + if not reloading then + if input:IsMouseButtonPressed(Enum.UserInputType.MouseButton2) or input:IsKeyDown(Enum.KeyCode.Q) then + aiming = true + c.setRunning(false) + rc.target = ra.aiming + lc.target = la.aiming + fov = (fov+60)/2 + else + aiming = false + fov = (fov+70)/2 + rc.target = ra.idling + rc.janky = CFrame.new():Lerp(game.Workspace.CurrentCamera.CFrame.Rotation:ToObjectSpace(cameraLast),0.3) + cameraLast = game.Workspace.CurrentCamera.CFrame.Rotation + --if lc.target ~= la.resting then c.setArm(l) end + if returnTable.shouldRest then lc.target = la.resting + else lc.target = la.idling end + end + game.Workspace.CurrentCamera.FieldOfView = fov + if c.running then + rc.target = CFrame.new(1.5,-1,-1.5) * CFrame.Angles(1,0,0) + lc.target = la.resting + end + if not reloading and input:IsKeyDown(Enum.KeyCode.R) then + mode = modes.reload + reloading = true + modes.reload() + mode = modes.idle + reloading = false + returnTable.rounds = returnTable.maxRounds + end + end + if not c.humanoid.SeatPart then c.rotateCharacter() c.disableMotors() else --[[print("Enable")]] c.enableMotors() end + c.targets[r] = rc.target * rc.offset * rc.janky + c.targets[l] = lc.target * lc.offset * lc.janky + if gun.Gun:FindFirstChild("Slide") and gun.Gun.Slide:FindFirstChild("Motor6D") then + gun.Gun.Slide.Motor6D.C1 = gun.Gun.Slide.Motor6D.C1:Lerp(gc.target,0.5) + end + c.setArm(r,delta) + if lc.target == la.resting then + c.restArm(l,delta) + else + c.setArm(l,delta) + end + c.motors[r].Part1.LocalTransparencyModifier = 0 + c.motors[l].Part1.LocalTransparencyModifier = 0 + c.motors[r].Part1.CastShadow = false + c.motors[l].Part1.CastShadow = false + if math.abs(game.Workspace.CurrentCamera.CFrame:ToObjectSpace(c.head.CFrame).Position.Z) > 1 then c.cameraOffset = Vector3.new(1,0,0) end + end) + c.run:BindToRenderStep("Transparency",2,function() + --game.StarterPlayer.CameraMode = Enum.CameraMode.LockFirstPerson + end) + end) + return returnTable +end +return getfenv() \ No newline at end of file diff --git a/scripts/modules/HexxieModule.lua b/scripts/modules/HexxieModule.lua new file mode 100644 index 0000000..024ba90 --- /dev/null +++ b/scripts/modules/HexxieModule.lua @@ -0,0 +1,69 @@ +local extract = bit32.extract +local replace = bit32.replace +local gmatch = string.gmatch +local char = string.char +local pck = string.pack +local unpck = string.unpack +local lshift = bit32.lshift +local rshift = bit32.rshift +local format = string.format +local byte = string.byte +local tostring = tostring +local tonumber = tonumber +local gsub = string.gsub +local rep = string.rep +local sub = string.sub +local r = 33 + +@native +local function encode(num) + local str = pck("d",num) + local f = unpck("I",str:sub(1,4)) + local s = unpck("I",str:sub(5,8)) + return format("%s%s%s%s%s%s%s%s%s%s%s", + char(extract(f,0,6)+r), + char(extract(f,6,6)+r), + char(extract(f,12,6)+r), + char(extract(f,18,6)+r), + char(extract(f,24,6)+r), + char(extract(s,0,6)+r), + char(extract(s,6,6)+r), + char(extract(s,12,6)+r), + char(extract(s,18,6)+r), + char(extract(s,24,6)+r), + char(replace(extract(f,30,2),extract(s,30,2),2,2)+r) + ) +end +@native +local function decode(str) + local a,b,c,d,e,f,g,h,i,j,k = byte(str,1,11) + k = k - r + local s = replace(replace(replace(replace(replace(f-r,g-r,6,6),h-r,12,6),i-r,18,6),j-r,24,6),extract(k,2,2),30,2) + local f = replace(replace(replace(replace(replace(a-r,b-r,6,6),c-r,12,6),d-r,18,6),e-r,24,6),extract(k,0,2),30,2) + local a = unpck("d",pck("I",f)..pck("I",s)) + return a +end +@native +local function rle_encode(str) + --print("nuttin",str) + str = gsub(str,"%$","$$") + --print("dollars sanitised",str) + str = gsub(str,"(!!!!?!?!?!?!?!?)",function(rep) + return format("$%i",#rep) + end) + --print("repeats replaced",str) + return str +end +@native +local function rle_decode(str) + str = gsub(str,"(%$+)(%d)",function(first,num) + if (#first % 2 == 0) then return first..num end + return rep("$",#first-1)..rep("!",tonumber(num)) + end) + --print("repeats removed",str) + str = gsub(str,"%$%$","$") + --print("dollars desanities",str) + return str +end + +return {decode = decode, encode = encode, rle_encode = rle_encode, rle_decode = rle_decode} diff --git a/scripts/modules/IdModule.lua b/scripts/modules/IdModule.lua new file mode 100644 index 0000000..d25c367 --- /dev/null +++ b/scripts/modules/IdModule.lua @@ -0,0 +1,182 @@ +-- This module can be required and used by other scripts to get information about where the mouse is in the 3D world +local module = {} + +module.tags = _G.Tags +-- table of parts in game by index, each index is the object's id, each object has an ID tag associated to it +--[[module.objects = {} -- this should only be regarded for the server + +function module.getId(object) -- CLIENT/SERVER + for _,tag in pairs(module.tags:GetTags(object)) do -- remove object from the objects table by its ID + if string.sub(tag,1,3) == "ID_" then + local id = tonumber(string.sub(tag,4,#tag)) + return id + end + end + return false +end + +function module.removeId(object) -- SERVER ONLY + module.objects[module.getId(object)] = nil +end + +function module.newId(object) -- SERVER ONLY + local currentIdIndex = 1 + for i=1,#module.objects do + if not module.objects[i] then + module.objects[i] = object + return i + end + end + module.objects[#module.objects+1] = object + return #module.objects+1 +end]] + +local function getConnectedParts(part) + local connected = part:GetConnectedParts(false) + for _,joint in pairs(part:GetJoints()) do + if joint:IsA("Constraint") then + if joint.Attachment0 and joint.Attachment1 then + if joint.Attachment0.Parent == part then + table.insert(connected,joint.Attachment1.Parent) + else + table.insert(connected,joint.Attachment0.Parent) + end + end + end + end + return connected +end + +function module.forEachConnectedObject(start,callback,show,parent,ignore) + if not start then return end + if start:IsA("Model") then start = start.PrimaryPart end + + if not parent then parent = game.Workspace.Objects end + + local parts = {} + local objects = {} + + local function checkPart(part) + local object + if part.Parent:IsA("Model") and part.Parent.Parent == parent then + object = part.Parent + elseif part.Parent == parent then + object = part + end + if not object or objects[object] then return end + objects[object] = object + callback(object) + end + + local function tryConnected(part) + if parts[part] then return else parts[part] = part checkPart(part) end + local connected = getConnectedParts(part) + for _,part in pairs(connected) do + tryConnected(part) + end + end + + tryConnected(start) + + return objects +end + +--[[function module.forEachConnectedObjectOld2(start,callback,show,parent,ignore) + if not start then return end + if start:IsA("Model") then start = start.PrimaryPart end + + if not parent then parent = game.Workspace.Objects end + local checkedTag = "checked"..tostring(tick()) + + local objects = {} + + local function checkPart(part) + local object + if part.Parent:IsA("Model") and part.Parent.Parent == parent then + object = part.Parent + elseif part.Parent == parent then + object = part + end + if object and not object:HasTag(checkedTag) then + object:AddTag(checkedTag) + table.insert(objects,object) + end + return object + end + + local function getConnected(part) + if not part:HasTag(checkedTag) then + checkPart(part) + part:AddTag(checkedTag) + for _,part in pairs(getConnectedParts(part)) do + if part.Name ~= "Separator" or not ignore then getConnected(part) end + end + end + end + + for _,part in pairs(start:GetConnectedParts(false)) do + getConnected(part) + end + + for _,object in pairs(objects) do + callback(object) + end + + for _,part in pairs(module.tags:GetTagged(checkedTag)) do + module.tags:RemoveTag(part,checkedTag) + end +end + +function module.forEachConnectedObjectOld(start,callback,debugShow,parent) + local tagId = "checked"..tostring(tick()) + if start:IsA("Model") then start = start.PrimaryPart end + local parts = start:GetConnectedParts(true) + module.tags:AddTag(start,tagId) + module.tags:AddTag(start.Parent,tagId) + local function checkPart(part,depth) + local count = 0 + if not module.tags:HasTag(part,tagId) then + if depth < 2 then + for _,joint in pairs(part:GetJoints()) do + if joint:IsA("Constraint") then + if joint.Attachment1 and not module.tags:HasTag(joint.Attachment1.Parent,tagId) then + for _,part in pairs(joint.Attachment1.Parent:GetConnectedParts(true)) do + checkPart(part,depth+1) + end + end + if joint.Attachment0 and not module.tags:HasTag(joint.Attachment0.Parent,tagId) then + for _,part in pairs(joint.Attachment0.Parent:GetConnectedParts(true)) do + checkPart(part,depth+1) + end + end + end + end + end + local newObject -- Note, we are walking over the newObject function. this is ok, we're not gonna use it. a neat feature of lua's scope + if part.Parent == game.Workspace.Objects then -- we found a part! lets add it + newObject = part + elseif part.Parent:IsA("Model") and part.Parent.Parent == game.Workspace.Objects then -- we found a model instead from this part (its a child of the model) + newObject = part.Parent + end + print(part,depth) + if newObject and not module.tags:HasTag(newObject,tagId) then + module.tags:AddTag(newObject,tagId) + callback(newObject) + if debugShow then + local previousTransparency = part.Transparency -- fancy bits here show for debug + part.LocalTransparencyModifier = 0.9 - (depth * 0.2) + if count == 6 then count = 0 wait() else count = count + 1 end + task.delay(0.25,function() part.LocalTransparencyModifier = previousTransparency end) -- fancy bits here show for debug + end + end + end + end + for key,part in pairs(parts) do + checkPart(part,0) + end + for _,part in pairs(module.tags:GetTagged(tagId)) do + module.tags:RemoveTag(part,tagId) + end +end]] + +return module diff --git a/scripts/modules/InputModule.lua b/scripts/modules/InputModule.lua new file mode 100644 index 0000000..e5ccf73 --- /dev/null +++ b/scripts/modules/InputModule.lua @@ -0,0 +1,213 @@ +require(game.ReplicatedFirst.ReadyModule) +local module = {} +local input = game:GetService("UserInputService") +local mouse = require(_G.Modules.MouseModule) + +local gamepad = Enum.UserInputType.Gamepad1 +local function KeyBinding(keycode,value) + return function() + if input:IsKeyDown(keycode) then return value else return nil end + end +end +local function ButtonBinding(keycode,value) + return function() + if input:IsGamepadButtonDown(gamepad,keycode) then return value else return nil end + end +end +local function MouseBinding(inputtype,value) + return function() + if input:IsMouseButtonPressed(inputtype) then return value else return nil end + end +end + +local function SequenceBinding(iterations,predicate) + local value = 0 + input.InputEnded:Connect(function(input,irrelevant) + if not irrelevant then + if predicate(input) then + value = value + 1/iterations + if value > 1 then value = 0 end + return value + end + end + end) + return function() return value end +end + +local function SequenceKeyBinding(keycode,iterations) + return SequenceBinding(iterations,function(input) + return input.UserInputType == Enum.UserInputType.Keyboard and input.KeyCode == keycode + end) +end +local function SequenceButtonBinding(keycode,iterations) + return SequenceBinding(iterations,function(input) + return input.UserInputType == gamepad and input.KeyCode == keycode + end) +end +local function SequenceMouseBinding(inputtype,iterations) + return SequenceBinding(iterations,function(input) + return input.UserInputType == inputtype + end) +end + +local function HoldBinding() + return function(old,new) + if not new then return old end + end +end + +local function DeltaBinding(velocity,predicate) + return function(value,new,delta) + if predicate() then return (value or 0) + velocity * delta end + end +end + +local function DeltaKeyBinding(keycode,velocity) + return DeltaBinding(velocity,function() + return input:IsKeyDown(keycode) + end) +end +local function DeltaButtonBinding(keycode,velocity) + return DeltaBinding(velocity,function() + return input:IsGamepadButtonDown(gamepad,keycode) + end) +end +local function DeltaMouseBinding(inputtype,velocity) + return DeltaBinding(velocity,function() + return input:IsMouseButtonPressed(inputtype) + end) +end + +local function MouseMovement(keycode) + return function() + input:IsKeyDown(keycode) + end +end + +local controlBindings = { + throttle = { + DeltaKeyBinding(Enum.KeyCode.LeftShift,1), + DeltaKeyBinding(Enum.KeyCode.LeftControl,-1), + HoldBinding() + }, + pitch = { + KeyBinding(Enum.KeyCode.W,-1), + KeyBinding(Enum.KeyCode.S,1) + }, + yaw = { + KeyBinding(Enum.KeyCode.Q,1), + KeyBinding(Enum.KeyCode.E,-1) + }, + roll = { + KeyBinding(Enum.KeyCode.A,1), + KeyBinding(Enum.KeyCode.D,-1) + }, + fire = { + MouseBinding(Enum.UserInputType.MouseButton1,1), + KeyBinding(Enum.KeyCode.F,-1) + }, + bomb = { + KeyBinding(Enum.KeyCode.B,1) + }, + action1 = { + ButtonBinding(Enum.KeyCode.L,1) + }, + action2 = { + MouseBinding(Enum.UserInputType.MouseButton3,1) + }, + flaps = { + SequenceKeyBinding(Enum.KeyCode.F,4) + }, + gear = { + SequenceKeyBinding(Enum.KeyCode.G,1) + }, +} + +module.values = {} +module.cursor = nil + +function module.aimTowards(root,position,values) + local aim = root:PointToObjectSpace(position).Unit + local _,_,z = root:ToEulerAnglesYXZ() + values.pitch += aim.Y*2 + values.pitch + values.yaw += aim.X*-2 + values.yaw + values.roll += values.roll + (-z/3) + values.yaw/5 +end + +-- Bindings +local shouldAim = true +module.seat = nil +if _G.IsClient then + spawn(function() + local dragger = require(_G.Modules.DraggingModule).dragger + _G:WaitFor("ControlMouseButton") + local function aimOptionCallback() + shouldAim = not shouldAim + if shouldAim then + _G.ControlMouseButton.Text = "Mouse" + else + _G.ControlMouseButton.Text = "WASD" + end + end + _G.ControlMouseButton.Activated:Connect(aimOptionCallback) + input.InputEnded:Connect(function(input,irrelevant) + if not irrelevant then + if + input.UserInputType == Enum.UserInputType.Keyboard and + input.KeyCode == Enum.KeyCode.LeftAlt + then + aimOptionCallback() + end + end + end) + local function processThrottle(throttle) + throttle = math.clamp(throttle,-1,1) + _G.ThrottleFrame.ThrottleBar.Value.Size = UDim2.new(1, 0, 0.5 * math.abs(throttle),0) + _G.ThrottleFrame.ThrottleBar.Value.Position = UDim2.new(0, 0, 0.5 - (0.5 * math.clamp(throttle,0,1)),0) + _G.ThrottleFrame.TextValue.Text = tostring(math.ceil(throttle * 100)).."%" + if throttle > 0 then + _G.ThrottleFrame.ThrottleBar.Value.BackgroundColor3 = Color3.new(0, 1, 0) + end + if throttle < 0 then + _G.ThrottleFrame.ThrottleBar.Value.BackgroundColor3 = Color3.new(1, 0, 0) + end + module.values.throttle = throttle + end + dragger(_G.ThrottleFrame.ThrottleBar,function(begin) + return { + drag = function(changed) + local down = (changed.Y - (_G.ThrottleFrame.ThrottleBar.AbsolutePosition.Y))/_G.ThrottleFrame.ThrottleBar.AbsoluteSize.Y + processThrottle(-(math.clamp(down,0,1) * 2 - 1)) + end, + } + end) + repeat + local delta = task.wait() + _G.ThrottleFrame.Visible = not not module.seat + if not module.seat then continue end + for name,bindings in pairs(controlBindings) do + local value = nil + for _,binding in pairs(bindings) do + local new = binding(module.values[name],value,delta) + if new then + --print(name,module.values[name],value,delta) + if math.abs(new) > (value or 0) then value = new end + end + end + module.values[name] = value or 0 + end + local instance,ray = mouse.getMouseHit() + module.cursor = ray.Origin + ray.Direction * 300 + if module.seat and shouldAim then + local position = ray.Origin + ray.Direction * 300 + Vector3.new(0,25,0) + module.aimTowards(module.seat:GetPivot(),position,module.values) + end + processThrottle(module.values.throttle) + for index,value in pairs(module.values) do + module.values[index] = math.clamp(value or 0,-1,1) + end + until false + end) +end + +return module \ No newline at end of file diff --git a/scripts/modules/IslandTerrain.lua b/scripts/modules/IslandTerrain.lua new file mode 100644 index 0000000..a096ac1 --- /dev/null +++ b/scripts/modules/IslandTerrain.lua @@ -0,0 +1,176 @@ +params = {} +params.cs = 512 +params.cliff = 4 +params.offset = Vector3.new(0,-300,0) +params.thresh = 0.18 +params.voxels = false +local p = params + +exclude = {} + +chunks = {} + +function part(x,y,z,mat,s) + local stone = Instance.new("Part") + stone.Parent = game.Workspace.Generation + stone.Transparency = 0 + stone.Material = Enum.Material.Slate + stone.Position = Vector3.new(x,y,z)*params.s + p.offset + stone.Size = Vector3.new(p.s,p.s,p.s) + if s then stone.Size = s end + stone.Anchored = true + + local grass = stone:Clone() + grass.CanCollide = true + grass.Transparency = 0 + grass.Material = Enum.Material.Grass + grass.Color = Color3.fromRGB(255, 255, 255) + grass.Parent = stone + grass.Size = Vector3.new(stone.Size.X+1,20,stone.Size.Z+1) + grass.Position = stone.Position + Vector3.new(0,stone.Size.Y/2 - grass.Size.Y/2 + 1,0) + return stone +end +local mod = 1 +local n = function(x,y,z,scale) + return math.noise(x*scale,y*scale,z*scale) +end +local function mulv(a,b) return Vector3.new(a.x*b.x,a.y*b.y,a.z*b.z) end +local structures = _G.Storage.Structures:GetChildren() +local foliage = _G.Storage.Foliage:GetChildren() +function islandChunk(ox,oz) -- i had to think there lol + local chunk = chunks[tostring(ox)..","..tostring(oz)] + local cn = n(ox,.5,oz,0.05) + if cn < 0.3 then return end + local offset = math.abs(n(ox,124.5432,oz,0.015)*2) + ox = ox + 20 + for oy = 0,3 do + mod = math.clamp(math.pow(Vector2.new(ox,oz).Magnitude/6,2),0,1) + local height = {} + p.thresh = 0.5 + p.s = 10 + local lx = 1/8 + local ly = 1/8 + local lz = 1/8 + local values = {} + local xi = 0 + for x=ox,ox+1,lx do + xi = xi + 1 + table.insert(values,{}) + local yi = 0 + for y=oy,oy+1,ly do + yi = yi + 1 + local hshift = math.abs((y/2)-1)/4 + table.insert(values[xi],{}) + local zi = 0 + for z=oz,oz+1,lz do + zi = zi + 1 + local v = (n(x,y,z,0.05))*((n(x,y,z,0.1)+n(x,y,z,1)+n(x,y,z,0.5)))-0.2-hshift-math.abs(n(x,0.123,y,0.1)) + table.insert(values[xi][yi],v) + if xi > 1 and yi > 1 and zi > 1 then + local temp = values[xi] + local a = temp[yi][zi-1] > 0 + local b = temp[yi-1][zi] > 0 + local c = values[xi-1][yi][zi] > 0 + local d = v > 0 + if ((not(a and b and c) and d) or ((a or b or c) and not d)) and math.random(1,4) == 1 then + local part = _G.Storage.Stone:Clone() + part.Position = Vector3.new(x,y+offset,z)*params.cs + part.Size = Vector3.new(lx,ly,lz)*params.cs + part.Parent = chunk.folder + local m = Vector3.new(math.random(1,16)/4,math.random(1,16)/4,math.random(1,16)/4)+Vector3.new(1.2,1.2,1.2) + part.Size = mulv(part.Size,m) + + local grass = _G.Storage.Grass:Clone() + grass.Size = Vector3.new(part.Size.X+1,params.cs/math.random(5,30),part.Size.Z+1) + grass.CFrame = part.CFrame + Vector3.new(0,part.Size.Y/2,0) + grass.Parent = chunk.folder + end + end + end + end + end + end +end +function chunk(ox,oz) + math.randomseed(ox+oz) + local chunk = {} + chunk.folder = Instance.new("Folder") + chunk.folder.Name = tostring(ox)..", "..tostring(oz) + chunk.folder.Parent = game.Workspace.Generation + chunk.position = Vector3.new(ox,0,oz) -- ? my hands are cold again + chunk.needed = true + if ox%2==0 and oz%2==0 then + for i=18,2,-4 do + local water = _G.Storage.Water:Clone() + water.Size = Vector3.new(params.cs,2,params.cs)*2 + water.Position = Vector3.new(ox,0,oz)*params.cs + Vector3.new(0,i+-41.5,0) + water.Parent = chunk.folder + end + end + chunks[tostring(ox)..","..tostring(oz)] = chunk -- again abusing Lua's hashtables + --[[islandChunk(ox,oz) -- oh lord have mercy on my clockspeed + local cn = n(ox,.5,oz,0.05) + if cn < 0.4 then return end + mod = math.clamp(math.pow(Vector2.new(ox,oz).Magnitude/6,2),0,1) + local height = {} + local lx = 1/8 + local ly = 1/8 + local lz = 1/8 + local values = {} + local xi = 0 + for x=ox-lx,ox+1,lx do + xi = xi + 1 + table.insert(values,{}) + local yi = 1 + local y=-0.05 + local hshift = y/2 + table.insert(values[xi],{}) + local zi = 0 + for z=oz-lz,oz+1,lz do + zi = zi + 1 + local v = (n(x,y,z,1)+n(x,y,z,1))*n(x,y,z,0.05)-0.5-hshift + table.insert(values[xi][yi],v) + if xi > 1 and zi > 1 then + if v > 0 then + local r = CFrame.new()--CFrame.Angles(0,math.random(-10,10)/300,0) + local part = _G.Storage.Sand:Clone() + part.CFrame = CFrame.new(Vector3.new(x,y,z)*params.cs)*r + part.Size = Vector3.new(lx,ly*0.2,lz)*params.cs + part.Parent = chunk.folder + + local grass = _G.Storage.Grass:Clone() + grass.CFrame = CFrame.new(Vector3.new(x,y,z)*params.cs)*r + grass.Size = Vector3.new(lx,ly*0.2,lz)*params.cs + grass.Parent = chunk.folder + + local m = Vector3.new(math.random(1,10),math.random(1,10)/10,math.random(1,10))/5+Vector3.new(1,1,1) + part.Size = mulv(part.Size,m) + grass.Size = mulv(grass.Size,(m+Vector3.new(0,0.3,0))*0.9) + m = Vector3.new(math.random(1,4)/4+1,1,math.random(1,4)/4+1) + part.Size = mulv(part.Size,m) + local random = Random.new(60391) + for sx=x,x+lx,0.125 do + for sz=z,z+lz,0.125 do + local fv = n(x+1/sx,0,z+1/sz,4.123) + if fv > 0.3 then + local sub = foliage[random:NextInteger(1,#foliage)]:Clone() + sub:SetPrimaryPartCFrame(CFrame.new(Vector3.new(sx,y,sz)*params.cs)*CFrame.Angles(0,math.random(0,3)*math.pi*0.5,0)) + sub.Parent = chunk.folder + sub:ScaleTo(math.random(1,10)/10 + 1) + end + if fv < -0.4 then + local sub = structures[random:NextInteger(1,#structures)]:Clone() + sub:SetPrimaryPartCFrame(CFrame.new(Vector3.new(sx-0.125,y+0.015+0.00390625,sz)*params.cs)*CFrame.Angles(0,math.random(0,3)*math.pi*0.5,0)) + sub.Parent = chunk.folder + end + end + end + end + end + end + end]] +end + +-- ok now more! + +return getfenv() \ No newline at end of file diff --git a/scripts/modules/MethodModule.lua b/scripts/modules/MethodModule.lua new file mode 100644 index 0000000..f59e715 --- /dev/null +++ b/scripts/modules/MethodModule.lua @@ -0,0 +1,975 @@ +local module = {} +_G.Methods = module + +local tags = _G.Tags + +local saveToPartTranslator = { + Barrel = "$B", + Sheet = "$S", + Stick = "$s" +} +local partToSaveTranslator = { + +} + +local ids = require(_G.Modules.IdModule) +module.Ids = ids +local mouse = require(_G.Modules.MouseModule) +module.Mouse = mouse + +local buildParts = {} +local idParts = {} +local idNodes = {} +local polyParts = {} +module.canLoad = false +module.polyParts = polyParts +module.buildParts = buildParts +module.idParts = idParts +task.spawn(function() + require(game.ReplicatedFirst.ReadyModule)("Parts") + _G.Storage:WaitForChild("Parts") + for _,object in pairs(_G.Storage.Parts:GetDescendants()) do + if object.Parent:IsA("Folder") then + local id = object:GetAttribute("PartId") + if not id then continue end + if not object:HasTag("ConnectorObject") then + buildParts[object.Name] = object + if saveToPartTranslator[object.Name] then buildParts[saveToPartTranslator[object.Name]] = object end + idParts[id] = object + else + if object:GetAttribute("Nodes") == 3 then + if not object:FindFirstChild("A") then + warn(tostring(object).." missing sub-wedge 'A'") + end + if not object:FindFirstChild("B") then + warn(tostring(object).." missing sub-wedge 'B'") + end + end + idParts[id] = object + polyParts[id] = object + polyParts[object.Name] = object + --[[if object:FindFirstChild("A") and object:FindFirstChild("B") then + polyParts[object.Name] = object + polyParts[id] = object + elseif object:FindFirstChild("1") and object:FindFirstChild("2") then + idParts[id] = object + else + polyParts[object.Name] = object + polyParts[id] = object + idParts[id] = object + end]] + end + end + end + -- To fix old compatibility issues + local polyCompat = { + Pane = 17, + Wedge = 6, + Stick = 5, + Paper = 20, + HeavySheet = 10, + Bar = 13, + LightSheet = 14, + Rod = 13, + Rope = 22 + } + for index,item in pairs(polyCompat) do + --[[local part = idParts[item]:Clone() + part.Parent = idParts[item].Parent + if part:FindFirstChild("1") then part["1"].Name = "A" end + if part:FindFirstChild("2") then part["2"].Name = "B" end]] + polyParts[index] = polyParts[item] + end + module.canLoad = true +end) +for id,part in pairs(idParts) do + local nodeId = part:GetAttribute("NodeId") + if nodeId then + idNodes[id] = idParts[nodeId] + end +end +function module.getPartByName(name) + return buildParts[name] +end +function module.getPolyByName(name) + return polyParts[name] +end +function module.getPartById(id) + return idParts[id] +end +function module.getNodeById(id) + return idNodes[id] +end + +function module.setItemCFrame(item,cframe) + if item:IsA("BasePart") then + item.CFrame = cframe + elseif item:IsA("Model") then + if item.PrimaryPart then + local mcframe,size = item:GetBoundingBox() + item:SetPrimaryPartCFrame(cframe-cframe.Position+cframe:PointToWorldSpace(mcframe:PointToObjectSpace(item.PrimaryPart.Position))) + end + end + return cframe +end + +function module.getItemSize(item) + if item:IsA("BasePart") then + return item.Size + elseif item:IsA("Model") then + if item.PrimaryPart then + return item:GetExtentsSize() + end + end +end + +function module.getItemCFrame(item) + if item:IsA("BasePart") then + return item.CFrame + elseif item:IsA("Model") then + return item:GetBoundingBox() + end +end + +function module.colorPart(part,color) + if part:IsA("Model") then + for _,subpart in pairs(part:GetDescendants()) do + if subpart:IsA("BasePart") then + subpart.Color = color + end + end + else + part.Color = color + end + part:SetAttribute("Color",color) +end + +function module.shapeTriangle(p1,p2,p3,r1,r2) -- simple function, might work? + local s = 0 --0.01 + local bottom = { + b1 = nil, + b2 = nil, + dist = 0, + other = nil + } + local function check(point1,point2,other) + local dist = (point1 - point2).Magnitude + if dist > bottom.dist then + bottom.b1 = point1 + bottom.b2 = point2 + bottom.other = other + bottom.dist = dist + end + end + check(p1,p2,p3) -- check all of the different options for bottom and get the right one? + check(p1,p3,p2) + check(p2,p3,p1) + local view = CFrame.lookAt(bottom.b1,bottom.b2,(bottom.other-bottom.b1).Unit) + local object = view:PointToObjectSpace(bottom.other) + local length = object.Z + local height = object.Y + r1.Size = Vector3.new(r1.Size.X,height-s,-length-s) + r1.CFrame = (view * CFrame.Angles(0,math.rad(180),0)) * CFrame.new(0,height/2+s/2,-length/2+s/2) + r2.Size = Vector3.new(r2.Size.X,height-s,-(-bottom.dist-length)-s) + r2.CFrame = view * CFrame.new(0,height/2+s/2,-(bottom.dist-length)/2+s/2) + return r1,r2 +end + +function module.positionPart(part,nodes,s) + local d = (nodes[1].Position - nodes[2].Position).Magnitude -- distance + local a = nodes[1].Position -- first node + local b = nodes[2].Position -- second node + if #nodes == 2 then -- stick on Z (XY thick) + part.CFrame = CFrame.lookAt(a,b) * CFrame.Angles(0,math.pi*0.5,0) + part.CFrame = part.CFrame + part.CFrame.RightVector * d * 0.5 + part.Size = Vector3.new(d,part.Size.Y,part.Size.Z) + end + if #nodes == 3 then -- triangle on YZ (X thick) + local c = nodes[3].Position -- third node + local p2 = part["B"] + local p1 = part["A"] + module.shapeTriangle(a,b,c,p1,p2) + end +end + +function module.shiftPart(part,shift) + shift = shift - (part:GetAttribute("Shift") or 0) + for _,part in pairs(part:GetChildren()) do + if part:IsA("BasePart") then + part.Position = part.Position + part.CFrame.RightVector * shift + end + end +end + +function module.getNodePositions(part) + +end + +function module.form_rod(pa,p1,p2) + local d = (p1 - p2).Magnitude -- distance + pa.CFrame = CFrame.lookAt(p1,p2) * CFrame.Angles(0,math.pi*0.5,0) + pa.CFrame = pa.CFrame + pa.CFrame.RightVector * d * 0.5 + pa.Size = Vector3.new(d,pa.Size.Y,pa.Size.Z) +end + +module.saves = require(script.Parent:WaitForChild("SerialisationModule")) + +spawn(function() + require(game.ReplicatedFirst.ReadyModule)("MethodCommands") + if _G.IsClient then + local commands = require(_G.Modules.CommandsModule) + commands.new("save format (%d)","set save format : Sets current save version. Use '4'",function(v) + _G.Events.Notification:Fire("Set Save Format","Set save format to: "..v) + _G.SaveFormat = v + end) + commands.new("save comp[^%s]- (.+)","set save compress : Enables or disables save compression for debugging. A bad idea.",function(v) + v = not not v:match("t") + _G.Events.Notification:Fire("Set Save Compress","Set save compress to: "..tostring(v)) + _G.SaveCompression = v + end) + end +end) + +function module.saves.modelToData(root) + if root then + if module.saves[_G.SaveFormat] then + local success,result = xpcall(module.saves[_G.SaveFormat].encode,function(err) + print("before") + _G.Error(string.format("Save V%s Error: %s",_G.SaveFormat,err)) + end,root,{ + compress = _G.SaveCompression + }) + if not success then return nil end + if #result.errors > 0 then print(result.errors) end + return result.data + else + return _G.Methods.dataToString(_G.Methods.modelToTable(root)) + end + else + error("Root passed is nil!",2) + end +end + +function module.saves.dataToModel(raw,physics,mirror) + if _G.IsString(raw) then + local v,r = raw:match("v(%d)save(.+)") + if v then + local success,result = xpcall(module.saves[v].decode,function(err) + warn(debug.traceback(err)) + end,r,{ + physics = physics, + mirror = mirror, + }) + if not success then return nil end + if #result.errors > 0 then print(result.errors) end + if result.model then result.model:SetAttribute("Version","V"..v) end + return result.model + else + local tabular = _G.Protected(module.universalConvertToTable,raw) + if not tabular or not tabular[1] then return nil end + local model = _G.Protected(module.convertToModel,tabular,physics) + if not model then return nil end + if not tabular[1]:match("V%dSAVE") then + tabular.Name = tabular[1] + --model:AddTag("Old") + end + model:AddTag("Old") + return model + end + else + return nil + end +end + +function module.tableToModel(data,physics) + +end + +function module.stringToData(str) + if type(str) ~= "string" then error("Not a string",2) end + local insert = table.insert + if str:sub(1,1) ~= "{" then return end + local function parse(start) + local data = {} + local function register(str) + if #str < 1 then return end + local index,part = str:match("(.*)=(.*)") + if not index then + insert(data,str) + else + data[index] = str + end + end + while start do + local first,last = str:find("[{},]",start+1,false) + local c = str:sub(last,last) + if c == "{" then + local sub_data + local index = str:sub(start+1,last-2) + sub_data,start = parse(last) + if #index > 0 then + data[index] = sub_data + else + insert(data,sub_data) + end + elseif c == "}" then + register(str:sub(start+1,last-1)) + return data,last + else + if c == "," then + register(str:sub(start+1,last-1)) + end + start = last + end + end + wait() + end + return parse(1) +end + +function module.can_destroy(part,player) + return part:HasTag("Combat") or part.Parent:HasTag("Combat") +end + +local built = {BuiltWeldConstraint = true} +function module.break_part(part,player) + if part:IsA("Model") then + for _,item in pairs(part:GetDescendants()) do + if built[item.Name] then + item:Destroy() + end + end + elseif part:IsA("BasePart") then + part:BreakJoints() + end +end + +function module.wash_part(part,colour,material) + if part:IsA("Model") then + for _,item in pairs(part:GetDescendants()) do + if part:IsA("BasePart") then + part.Color = colour + part.Material = material + end + end + elseif part:IsA("BasePart") then + part.Color = colour + part.Material = material + end +end + +function module.fire(root,player) + if module.can_destroy(root,player) then + if root:HasTag("Engine") then + local fire = _G.Storage.Fire:Clone() + fire.Parent = root.PrimaryPart + end + module.wash_part(root,Color3.new(1, 0.831617, 0.298161),Enum.Material.Neon) + wait(math.random(1,10)*0.333) + module.break_part(root,player) + for _,part in pairs(root:GetTouchingParts()) do + module.fire(part,player) + end + wait(math.random(1,20)*0.333) + module.wash_part(root,Color3.new(0,0,0),Enum.Material.Sand) + end +end + +function module.explode(root,player,power) + print("explode") + if root:HasTag("Exploded") then return end + root:AddTag("Exploded") + local debris = game:GetService("Debris") + if not power then power = 10 end + local explosion = _G.Sounds.Explosion:Clone() + explosion.Parent = root + explosion:Destroy() + local burn = _G.Storage.Burn:Clone() + burn.Parent = root + debris:AddItem(burn,1) + + local params = OverlapParams.new() + params.MaxParts = 40 + params.RespectCanCollide = true + params.FilterType = Enum.RaycastFilterType.Include + params.FilterDescendantsInstances = {game.Workspace.Objects} + local result = game.Workspace:GetPartBoundsInRadius(root.Position,10,params) + + wait() + for _,hit in pairs(result) do + if module.can_destroy(hit,player) then + module.break_part(hit,player) + if math.random(1,3) == 1 then module.fire(hit,player) end + end + end + + wait(0.1) + for _,hit in pairs(result) do + hit:ApplyImpulse((hit.Position-root.Position)*-100) + end +end + +function module.playerOwnsPart(player,part) return part:HasTag("P_"..player.UserId) or part:HasTag("P_ublic") end + +function module.dataToString(data) + local insert = table.insert + local isString = {["string"] = true} + local isNumber = {["number"] = true} + local isTable = {["table"] = true} + local function converts(t) + local result = {"{"} + local first = true + for index,item in pairs(t) do + if not first then insert(result,",") else first = false end + local value + if isTable[type(item)] then + value = converts(item) + else + value = tostring(item) + end + if isNumber[type(index)] then + insert(result,value) + elseif isString[type(index)] then + insert(result,index.."="..value) + end + end + insert(result,"}") + return table.concat(result) + end + return converts(data) +end + +function module.convertToData(data) + if not data:match("{.*}") then return end + data = data:gsub("%s","") + local index = 2 + local function parseTable(t) + local this = {} -- The table we're working with now + local idx -- Special named table index + local _,last + local char + local value + while true do + _,last = data:find("[{,=}]",index) + if not last then return end + char = data:sub(last,last) -- The next significant character + if index ~= last then value = data:sub(index,last-1) end -- Current value in table + index = last + 1 + --print(t,value,char) + local function terminate() + if idx then this[idx] = value -- If there is an index, add it by index + else table.insert(this,value) end -- Otherwise just plainly insert it (by order) + end + if char == "=" then -- When finding an '=', take the existing piece and make it an index + idx = value + elseif char == "," then -- When finding a ',', the entry has terminated so add it + terminate() + elseif char == "{" then -- When finding a '{', there is a new table whose value needs to be computed + value = parseTable(t+1) + else-- char == "}" then -- When finding a '}', this table has terminated so return + terminate() + return this + end + end + end + return parseTable(1) +end + +function module.universalConvertToTable(data) + if not (data:match("{V%dSAVE,objects=")) then return module.stringToData(data) + else return module.convertToData(data) end +end + +function module.removeSpaces(data) + local new = "" + for index = 1, #data do + local char = string.sub(data,index,index) + if char ~= " " then new = new .. char end + end + return new +end +function module.round(number,decimals) + local pow = math.pow(10,decimals) + return math.round(number*pow)/pow +end +function module.makeSaveString(data) + local saveDataString = "{"..data.saveName.."," + local saveObjectsString = "{" + for index,item in ipairs(data.saveObjects) do + local editString = "" + local cframeString = module.removeSpaces(tostring(item.CFrame)) + if item.Edit then + for _,edit in pairs(item.Edit) do + editString = editString .. "{"..edit[1]..","..edit[2].."}," + end + end + if #editString ~= 0 then editString = "{"..editString.."}," end + saveObjectsString = saveObjectsString .. "{{" + .. cframeString + .. "}" .. "," + .. item.Name .. ","..editString.."}," + end + saveObjectsString = saveObjectsString .. "}" + saveDataString = saveDataString .. saveObjectsString .. "," + local saveWeldsString = "{" + for index,item in pairs(data.saveWelds) do + saveWeldsString = saveWeldsString .. "{{" + .. tostring(item.Part0[1]) + if item.Part0[2] then + saveWeldsString = saveWeldsString .. "," .. item.Part0[2] + end + saveWeldsString = saveWeldsString .. "},{" + .. tostring(item.Part1[1]) + if item.Part1[2] then + saveWeldsString = saveWeldsString .. "," .. item.Part1[2] + end + saveWeldsString = saveWeldsString .. "}}," + end + saveWeldsString = saveWeldsString .. "}" + saveDataString = saveDataString .. saveWeldsString + saveDataString = saveDataString .. "}" + --print(saveDataString) + return saveDataString +end + +function module.convertValue(value) -- BRUH + if value == "true" then return true + elseif value == "false" then return false + elseif tonumber(value) then return tonumber(value) + else return value end +end +function module.tableToCFrame(c) + local n = tonumber + return CFrame.new( + n(c[1]),n(c[2]),n(c[3]), + n(c[4]),n(c[5]),n(c[6]), + n(c[7]),n(c[8]),n(c[9]), + n(c[10]),n(c[11]),n(c[12]) + ) +end + +local function weld(part1,part2) + local weld = Instance.new("WeldConstraint") + weld.Name = "BuiltWeldConstraint" + weld.Parent = part1 + weld.Part0 = part1 + weld.Part1 = part2 + return weld +end +local function weldNoPersist(part1,part2) + weld(part1,part2).Name = "WeldConstraint" +end +local CFrameNew = CFrame.new +local CFrameFromAxisAngle = CFrame.fromAxisAngle +local tableUnpack = table.unpack +local Vector3New = Vector3.new +local n = tonumber +function module.cframeToData(cframe) + local p = cframe.Position + local r,w = cframe:ToAxisAngle() + return {{cframe.Position},{r},w} +end +local tp = table.pack +function module.cframeToDataNoAxis(cframe) + return tp(cframe:GetComponents()) +end +local nc = CFrame.new +local tu = table.unpack +function module.dataToCFrameNoAxis(data) + return nc(tu(data)) +end +function module.dataToCFrame(data) + if not (data[1] and data[2] and data[3]) then return CFrame.new() end + local p1,p2,p3 = tableUnpack(data[1]) + local r1,r2,r3 = tableUnpack(data[2]) + local w = data[3] + return CFrameNew(n(p1),n(p2),n(p3))*CFrameFromAxisAngle(Vector3New(n(r1),n(r2),n(r3)),n(w)) +end + +local editCompress = {Color="C",HingeCFrame="HC",Size="S"} +local editDepress = {HC="HingeCFrame",C="Color",S="Size"} +function module.modelToTable(root) + local getItemCFrame = function(object) if object:IsA("BasePart") then return object.CFrame elseif object:IsA("Model") then return object.PrimaryPart.CFrame end end + local origin = getItemCFrame(root) + local insert = table.insert + local saveData = {"V3SAVE"} + saveData.objects = {} + saveData.welds = {} + local objects = {} + --[[Agreed object structure: + { + 1: name + 2: {node1,node2,node3} cframes (nodes included) + 3: {colour={a,b,c},size={a,b,c},speed=120} edits (size included) + } + ]] + local isBuiltWeld = {["BuiltWeldConstraint"] = true} + local index = 1 + ids.forEachConnectedObject(root,function(object) + insert(objects,object) + objects[object] = index + index = index + 1 + local cframes = {} + local edits = {} + local nodes = object:GetAttribute("Nodes") + if nodes then + for i=1,nodes do + insert(cframes,module.cframeToDataNoAxis(origin:ToObjectSpace(object[i].CFrame))) + end + else + insert(cframes,module.cframeToDataNoAxis(origin:ToObjectSpace(getItemCFrame(object)))) + end + -- edits + for index,edit in pairs(object:GetChildren()) do + if tags:HasTag(edit,"Edit") and + (edit:GetAttribute("Default") ~= edit.Value) + then + edits[editCompress[edit.Name] or edit.Name] = tostring(edit.Value) + end + end + local id = object:GetAttribute("PartId") or object.Name + --local original = module.getPartById(id) or module.getPartByName(id) + -- psuedo edits + local color = object:GetAttribute("Color") + if color then edits[editCompress["Color"]] = {color.R,color.G,color.B} end + local hinge = object:FindFirstChild("Hinge") + if hinge then edits[editCompress["HingeCFrame"]] = module.cframeToDataNoAxis(origin:ToObjectSpace(hinge.CFrame)) end + local size = object:GetAttribute("Size") + if size then size = size * math.sqrt(0.333); edits[editCompress["Size"]] = {size.X,size.Y,size.Z} end + + -- final object + local final = {id,cframes} + if next(edits) then insert(final,edits) end + insert(saveData.objects,final) + end) + + for index,object in ipairs(objects) do + for _,weld in pairs(object:GetDescendants()) do + if isBuiltWeld[weld.Name] then + if not weld.Part0 or not weld.Part1 then continue end + local first = objects[weld.Part0] + if not first then first = {objects[weld.Part0.Parent],weld.Part0.Name} + else first = {first} end + local second = objects[weld.Part1] + if not second then second = {objects[weld.Part1.Parent],weld.Part1.Name} + else second = {second} end + insert(saveData.welds,{first,second}) + end + end + end + + return saveData +end + +function module.convertToModel(data,physics) + if not data then return end + local v = data[1]:match("V(%d)SAVE") + if not v or not data.objects then return module.oldConvertToModel(data,physics) end + local insert = table.insert + local objects = {} + local model = Instance.new("Model") + local toCFrame = module.dataToCFrameNoAxis + local setCFrame = function(part,cframe) if part:IsA("BasePart") then part.CFrame = cframe else part:SetPrimaryPartCFrame(cframe) end end + model:SetAttribute("Version","V3") + if v == "2" then setCFrame = module.setItemCFrame toCFrame = module.dataToCFrame model:SetAttribute("Version","V2") end + local editActions = { + Color = function(edit,part) + module.colorPart(part,Color3.new(n(edit[1]),n(edit[2]),n(edit[3]))) + end, + HingeCFrame = function(edit,part) + local hinge = part:FindFirstChild("Hinge") + if hinge then hinge.CFrame = toCFrame(edit) end + end, + MaterialAttribute = function() + + end, + Size = function(edit,part) + local size = Vector3.new(n(edit[1]),n(edit[2]),n(edit[3])) + if part:IsA("Model") then + part:ScaleTo(size.Magnitude) + size = (size/math.sqrt(0.333)) -- For parity with the new stronger system + else + part.Size = size + end + part:SetAttribute("Size",size) + end, + } + repeat wait() until module.canLoad + for index,object in ipairs(data.objects) do + local name = object[1] + name = n(name) or name + local isPoly = #object[2] > 1 + local part = idParts[name] + local node + if not part then + if isPoly then + part = module.getPolyByName(name) + if not part then print(name) end + node = part.Parent:FindFirstChild("Node") or idParts[-1] + else part = module.getPartByName(name) end + else + if isPoly then + node = idNodes[name] or part.Parent:FindFirstChild("Node") or idParts[-1] + end + end + if not part then warn(name,isPoly,idParts,buildParts,polyParts) end + part = part:Clone() + local isModel = not part:IsA("BasePart") + local primary = part + if isModel then primary = part.PrimaryPart end + if isPoly then + local nodes = {} + for index,cframe in pairs(object[2]) do + local newNode = node:Clone() + newNode.CFrame = toCFrame(cframe) + newNode.Parent = part + newNode.Name = tostring(index) + newNode.Transparency = 1 + nodes[index] = newNode + end + module.positionPart(part,nodes) + for _,subpart in pairs(part:GetChildren()) do if subpart ~= primary and subpart:IsA("BasePart") then weldNoPersist(subpart,primary) end end + else + setCFrame(part,toCFrame(object[2][1])) + end + local cnv = module.convertValue + if object[3] then + for index,edit in pairs(object[3]) do + if editDepress[index] then index = editDepress[index] end + local value = part:FindFirstChild(index) + if not value then + if editActions[index] then + editActions[index](edit,part) + end + else + value.Value = cnv(edit) + end + end + end + if not physics then + if isModel then + for _,object in pairs(part:GetChildren()) do + if object:IsA("BasePart") then + object.Anchored = true + object.CanCollide = false + end + end + else + part.CanCollide = false + part.Anchored = true + end + end + if index == 1 then if part:IsA("Model") then model.PrimaryPart = part.PrimaryPart else model.PrimaryPart = part end end + if part.Name == "VehicleSeat" then model.PrimaryPart = part.PrimaryPart end + part.Parent = model + insert(objects,part) + end + if physics then + for index,weld in pairs(data.welds) do + local weld11 = n(weld[1][1]) + local weld12 = weld[1][2] + local weld21 = n(weld[2][1]) + local weld22 = weld[2][2] + local part1 = objects[weld11] + local part2 = objects[weld21] + if weld12 then part1 = part1:FindFirstChild(weld12) or part1 end + if weld22 then part2 = part2:FindFirstChild(weld22) or part2 end + if not (part1 and part2) then print("Weld Missing",objects[weld11],objects[weld21],weld21,weld22) continue end + local new = Instance.new("WeldConstraint") + if part1:IsA("Model") then part1 = part1.PrimaryPart end + if part2:IsA("Model") then part2 = part2.PrimaryPart end + new.Part0 = part1 + new.Part1 = part2 + new.Parent = part1 + new.Name = "BuiltWeldConstraint" + end + end + return model +end + +function module.oldConvertToModel(data,physics) + if not data[1] then return end + + local parts = {} + local n = tonumber + + local currentModel = Instance.new("Model") + currentModel:SetAttribute("Version","V1") + + local internalSuccess = true + local internalValue = {} + local success,value = xpcall(function() + for index,part in pairs(data[2]) do + local success,value = pcall(function() + local isPoly = #part[1] == 0 + local new = module.getPartByName(part[2]) + if new then new = new:Clone() end + if isPoly then + new = module.getPolyByName(part[2]) + if not new then warn("Missing Poly: "..part[2]) end + local old = new + new = new:Clone() + local realNodes = {} + local attach = new + if new:IsA("Model") then attach = new.PrimaryPart end + for _,edit in pairs(part[3]) do + if #edit[1] == 1 and tonumber(edit[1]) then + local realNode = (old.Parent:FindFirstChild("Node") or _G.Storage.Parts.DefaultNode):Clone() + realNode.CFrame = module.tableToCFrame(edit[2]) + realNode.Name = edit[1] + realNode.Parent = new + realNode.Transparency = 1 + table.insert(realNodes,realNode) + end + end + module.positionPart(new,realNodes) + for _,node in pairs(realNodes) do + weldNoPersist(node,attach) + end + for _,part in pairs(new:GetChildren()) do + if part:IsA("BasePart") and part ~= attach then weldNoPersist(part,attach) end + end + end + if new then + table.insert(parts,new) + new.Parent = currentModel + if index == 1 then + if new:IsA("Model") then + currentModel.PrimaryPart = new.PrimaryPart else currentModel.PrimaryPart = new + end + end + if part[3] then + local cnv = module.convertValue + if typeof(part[3]) == "string" then print(part[3]) end + for _,edit in pairs(part[3]) do + if #edit ~= 1 and not tonumber(edit[1]) and not (edit[1] == "C") and new:FindFirstChild(edit[1]) then + new[edit[1]].Value = module.convertValue(edit[2]) + elseif (edit[1] == "C") then + module.colorPart(new,Color3.new(cnv(edit[2][1]),cnv(edit[2][2]),cnv(edit[2][3]))) + end + end + end + if not physics then + if new:IsA("Model") then + new.PrimaryPart.Anchored = true + for _,part in pairs(new:GetDescendants()) do + if part:IsA("BasePart") then part.CanCollide = false part.CanTouch = false end + end + else + new.Anchored = true + new.CanCollide = false + new.CanTouch = false + end + end + if not isPoly then + module.setItemCFrame(new,module.tableToCFrame(part[1])) + end + end + end) + if not success then internalSuccess = false table.insert(internalValue,value) end + end + if not physics then return currentModel,data end + + for index,weld in pairs(data[3]) do + local new = Instance.new("WeldConstraint") + local part0 = parts[n(weld[1][1])] + if not part0 then warn("Weld missing.") continue end + if weld[1][2] and part0:FindFirstChild(weld[1][2]) then part0 = part0[weld[1][2]] end + local part1 = parts[n(weld[2][1])] + if not part1 then continue end + if weld[2][2] and part1:FindFirstChild(weld[2][2]) then part1 = part1[weld[2][2]] end + if part0:IsA("Model") then part0 = part0.PrimaryPart end + if part1:IsA("Model") then part1 = part1.PrimaryPart end + new.Part0 = part0 + new.Part1 = part1 + new.Parent = part0 + new.Name = "BuiltWeldConstraint" + end + end,print) + + if not internalSuccess then data[1] = "CORRUPTED" for _,item in pairs(internalValue) do print(data) print(item) debug.traceback(item,2) end end + + --print(data) + + return currentModel,data +end + +local function getConnectedParts(part) + local connected = part:GetConnectedParts(false) + for _,joint in pairs(part:GetJoints()) do + if joint:IsA("Constraint") then + if joint.Attachment0 and joint.Attachment1 then + if joint.Attachment0.Parent == part then + table.insert(connected,joint.Attachment1.Parent) + else + table.insert(connected,joint.Attachment0.Parent) + end + end + end + end + return connected +end + +function module.forEachConnectedObject(start,callback,show,parent) + if not start then return end + if start:IsA("Model") then start = start.PrimaryPart end + + if not parent then parent = game.Workspace.Objects end + + local parts = {} + local objects = {} + + local function checkPart(part) + local object + if part.Parent:IsA("Model") and part.Parent.Parent == parent then + object = part.Parent + elseif part.Parent == parent then + object = part + end + if not object or objects[object] then return end + objects[object] = object + if callback then callback(object) end + end + + local function tryConnected(part) + if parts[part] then return else parts[part] = part checkPart(part) end + local connected = getConnectedParts(part) + for _,part in pairs(connected) do + tryConnected(part) + end + end + + tryConnected(start) + + return objects +end + +function module.setNetworkOwner(part,player) + for _,part in pairs(part:GetConnectedParts(true)) do + if part.Anchored then + return + end + end + part:SetNetworkOwner(player) +end + +function module.getOwnedOrNil(player,part) + if module.playerOwnsPart(player,part) then + return part + elseif module.playerOwnsPart(player,part.Parent) then + return part + end + return nil +end + +function module.getObjectFromPart(part) + if part:GetAttribute("PartId") then + return part + elseif part.Parent and part.Parent:GetAttribute("PartId") then + return part.Parent + end +end + +module.getMouseHit = mouse.getMouseHit + +return module diff --git a/scripts/modules/MouseModule.lua b/scripts/modules/MouseModule.lua new file mode 100644 index 0000000..5531816 --- /dev/null +++ b/scripts/modules/MouseModule.lua @@ -0,0 +1,56 @@ +-- This module can be required and used by other scripts to get information about where the mouse is in the 3D world +local module = {} + +local input = game:GetService("UserInputService") +module.mouseDistance = 500 +module.mouseRaycastParameters = RaycastParams.new() + +local dominantFinger +local usingMouse = true +local lastMouseInstance +local lastMouseRay + +game:GetService("UserInputService").TouchStarted:Connect(function(touch,irrelevant) + if not irrelevant and not dominantFinger then + dominantFinger = touch + usingMouse = false + end +end) +game:GetService("UserInputService").TouchEnded:Connect(function(touch,irrelevant) + if touch == dominantFinger then dominantFinger = nil end +end) +local uit = Enum.UserInputType +local mouseInputs = {uit.MouseButton1,uit.MouseWheel,uit.MouseMovement,uit.MouseButton2} +for _,item in ipairs(mouseInputs) do mouseInputs[item] = true end +game:GetService("UserInputService").InputBegan:Connect(function(object) + if mouseInputs[object.UserInputType] then usingMouse = true end +end) + +function module.getMouseHit(location) + local mousePosition = location or input:GetMouseLocation() + local mouseRay = game.Workspace.CurrentCamera:ViewportPointToRay(mousePosition.X,mousePosition.Y) + if dominantFinger then + local position = dominantFinger.Position + mouseRay = game.Workspace.CurrentCamera:ScreenPointToRay(position.X,position.Y) + end + local mouseInstance = game.Workspace:Raycast(mouseRay.Origin,mouseRay.Direction*module.mouseDistance,module.mouseRaycastParameters) + return mouseInstance,mouseRay,(dominantFinger or usingMouse) +end + +function module.isMouseDown() + return input:IsMouseButtonPressed(Enum.UserInputType.MouseButton1) or dominantFinger +end + +function mouse() + local pos = input:GetMouseLocation() + local vec = game.Workspace.CurrentCamera:ViewportPointToRay(pos.x,pos.y,0) + return game.Workspace.CurrentCamera.CFrame:PointToObjectSpace(vec.Origin+vec.Direction*300).Unit +end + +function module.initialise() + module.mouseRaycastParameters.FilterDescendantsInstances = {game.Workspace.Hidden,game.Workspace.Players} + module.mouseRaycastParameters.FilterType = Enum.RaycastFilterType.Exclude + module.mouseRaycastParameters.IgnoreWater = true +end + +return module diff --git a/scripts/modules/NameColorModule.lua b/scripts/modules/NameColorModule.lua new file mode 100644 index 0000000..5e1baf4 --- /dev/null +++ b/scripts/modules/NameColorModule.lua @@ -0,0 +1,34 @@ +NAME_COLORS = + { + Color3.new(253/255, 41/255, 67/255), -- BrickColor.new("Bright red").Color, + Color3.new(1/255, 162/255, 255/255), -- BrickColor.new("Bright blue").Color, + Color3.new(2/255, 184/255, 87/255), -- BrickColor.new("Earth green").Color, + BrickColor.new("Bright violet").Color, + BrickColor.new("Bright orange").Color, + BrickColor.new("Bright yellow").Color, + BrickColor.new("Light reddish violet").Color, + BrickColor.new("Brick yellow").Color, + } + +local function GetNameValue(pName) + local value = 0 + for index = 1, #pName do + local cValue = string.byte(string.sub(pName, index, index)) + local reverseIndex = #pName - index + 1 + if #pName%2 == 1 then + reverseIndex = reverseIndex - 1 + end + if reverseIndex%4 >= 2 then + cValue = -cValue + end + value = value + cValue + end + return value +end + +local color_offset = 0 +function ComputeNameColor(pName) + return NAME_COLORS[((GetNameValue(pName) + color_offset) % #NAME_COLORS) + 1] +end + +return getfenv() diff --git a/scripts/modules/ObjectModule.lua b/scripts/modules/ObjectModule.lua new file mode 100644 index 0000000..0284b02 --- /dev/null +++ b/scripts/modules/ObjectModule.lua @@ -0,0 +1,435 @@ +-- Module for handling all game objects +require(game.ReplicatedFirst.ReadyModule) + +local input = require(_G.Modules.InputModule) + +-- Note that closures absorb upvalues. I'm trying this as a programming paradigm here. +local modulesByName = {} +local modules = {} -- [Name] = {...module} +local predicates = {} + +local objects = {} -- Table of globally considered parts (responsible for considering) (consider = input and background tasks) +local owning = {} -- Table of locally owned parts (responsible for all processing) + +-- Get the object from a part and do something to it +local function asObject(part,callback) + if part.Parent:GetAttribute("PartId") then return callback(part.Parent) -- Since parts can be INSIDE of objects + elseif part:GetAttribute("PartId") then return callback(part) end +end + +local processOwnership + +local context = { + controls = {}, + root = nil -- Where the system is controlling from (Seat) +} +context.message = function(instance,name,...) + local stuff = {...} + asObject(instance,function(object) + local data = objects[object] + if not data then return end + if name == "" then name = "default" end + name = name or "default" + for _,module in pairs(modules[object.Name] or {}) do + if module.messages then + if module.messages[name] then + if objects[object] then + module.messages[name](data,instance,context,table.unpack(stuff)) + end + end + end + end + end) +end +context.connected = function(wire_tag,end_tag,root,callback) + local searched = {} + + local search = {root} + while #search > 0 do + local new_search = {} + for _,search_part in pairs(search) do + local connected = search_part:GetConnectedParts() + for _,part in pairs(connected) do + if not searched[part] then + searched[part] = true + if part:HasTag(wire_tag) then + table.insert(new_search,part) + if part:GetAttribute("PartId") and part:HasTag("Glow") then part.Material = Enum.Material.Neon end + end + if (not end_tag) or part:HasTag(end_tag) or part.Parent:HasTag(end_tag) then + asObject(part,function(object) + callback(part) + end) + end + end + end + end + search = new_search + end +end +local queues = {} +context.queue = function(this,tag,delta,valid) + -- Init + valid = valid or owning + local queue = queues[tag] + if queue then queue = {last = tick()} queues[tag] = queue end + if not queue[this] then queue[this] = this table.insert(queue,1,this) end + -- Get next in line + local first = queue[1] + while not valid[first] do table.remove(queue,1) first = queue[1] end -- Remove removed objects (lol) + -- Are you ready!? + if tick() - queue.last > delta then -- Aye aye captain!! + queue.last = tick() -- Oooo + table.remove(queue,1) + table.insert(queue,this) + return true -- hhhh + else + return false + end +end +context.frequency = function(this,tag,delta) + if not this[tag] then this[tag] = tick() end + if tick() - this[tag] > delta then + this[tag] = tick() + return true + else + return false + end +end +context.asObject = asObject + +if _G.IsServer then + context.replicate = function(this,object,module,...) + module.replicate(this,object,context,...) + end +elseif _G.IsClient then + context.replicate = function(this,object,module,...) + if module.replicate then module.replicate(this,object,context,...) end + if tick() > module.next_replication then + module.next_replication = tick() + module.replication_delta + _G.Remotes.ObjectReplicate:FireServer(object,module.name,...) + end + end +end + +local thisPlayer +if _G.IsServer then + thisPlayer = nil +elseif _G.IsClient then + thisPlayer = game.Players.LocalPlayer +end + +-- Make object data from an instance (it's shared, so modules can communicate easily) +local function new(object) + local this = {} + for module,predicate in pairs(predicates) do + if predicate(object) then + local tab = modules[object.Name] + if not tab then tab = {} modules[object.Name] = tab end + tab[module] = module + end + end + for _,module in pairs(modules[object.Name] or {}) do + _G.Protected(module.new,this,object,context) + end + objects[object] = this + return this +end + +local function remove(object) + local this = objects[object] + if not this then return end + for _,module in pairs(modules[object.Name] or {}) do + if module.remove then _G.Protected(module.remove,this,object,context) end + end + objects[object] = nil + owning[object] = nil +end + +local own +-- Add an object to be considered by this client or server +local function add(instance,owner) + -- If this is not an instance or we already considered it then ignore + local data = objects[instance] + if data then return data end + if not instance:GetAttribute("PartId") then return end + data = new(instance) + objects[instance] = data + instance.Destroying:Connect(function() + remove(instance) + end) + if _G.IsServer then + if owner then + data.owner = owner + _G.Protected(function() + if instance:IsA("Model") then + instance.PrimaryPart:SetNetworkOwner(owner) + if instance:FindFirstChild("Hinge") then + instance.Hinge:SetNetworkOwner(owner) + end + else + instance:SetNetworkOwner(owner) + end + end) + else + own(instance) + end + else + if owner then own(instance) end + end + return data +end + +local function update(instance) + local data = objects[instance] + if data then + for _,module in pairs(modules[instance.Name] or {}) do + if module.update then _G.Protected(module.update,data,instance,context) end + end + end +end + +-- Make the client drop ownership and continue to run idle tasks +local function sleep(object) + if add(object) then owning[object] = nil end +end + +-- Make the client own the object and take responsibility for processing +own = function(object) + if add(object) then owning[object] = objects[object] end +end + +-- Get all parts connected to an instance, determine if the player is still connected in any way +local hrp = {HumanoidRootPart = true} +local function playerInSeat(seat) + if (seat:IsA("Seat") or seat:IsA("VehicleSeat")) and seat.Occupant then + return game.Players:GetPlayerFromCharacter(seat.Occupant.Parent) + else + local seat = seat:FindFirstChildWhichIsA("Seat") or seat:FindFirstChildWhichIsA("VehicleSeat") + if seat and seat.Occupant then + return game.Players:GetPlayerFromCharacter(seat.Occupant.Parent) + end + end +end + +local vs = {VehicleSeat = true} +local function maxRankSeat(seat1,seat2) + if not seat1 then return seat2 end + if not seat2 then return seat1 end + local priority1 = seat1:GetAttribute("Priority") or 0 + local priority2 = seat2:GetAttribute("Priority") or 0 + -- A seat has priority over the other + if priority1 == priority2 then + local class1 = vs[seat1.Name] + local class2 = vs[seat2.Name] + -- One seat is a VehicleSeat and the other isn't + if class1 and not class2 then return seat1 + elseif class2 and not class1 then return seat2 + else + local owner1 = game.Players:GetPlayerByUserId(seat1:GetAttribute("Owner") or 0) == playerInSeat(seat1) + local owner2 = game.Players:GetPlayerByUserId(seat2:GetAttribute("Owner") or 0) == playerInSeat(seat2) + -- One player is seating in a seat that they placed themselves + if owner1 and not owner2 then return seat1 + elseif owner2 and not owner1 then return seat2 + else return seat1 -- Since it was probably closest + end + end + elseif priority1 > priority2 then return seat1 else return seat2 end +end + +local function getParts(instance) + local searchedParts = {} -- Searched connected instance parts + local objectsFound = {} -- Found connected objects + local assemblyRoots = {} -- Found AssemblyRootParts + local playersSeated = {} -- Players that are connected to this vehicle in some way and their associated seat + -- Recursive function to look through all the parts + local function search(part) + -- DRY + local function add(instance) + searchedParts[instance] = true + local root = instance.AssemblyRootPart + if not assemblyRoots[root] then assemblyRoots[root] = root end + asObject(instance,function(object) + if instance:FindFirstChild("SeatWeld") then + local hrp = instance.SeatWeld.Part1 + local player = game.Players:GetPlayerFromCharacter(hrp.Parent) + if player then + -- Players could be in multiple seats so just pick the highest one + playersSeated[player] = maxRankSeat(playersSeated[player],object) + end + end + objectsFound[object] = object + end) + end + -- If not found, add it, otherwise return + if not searchedParts[part] then + add(part) + else return end + -- For all the connected parts do the same thing, but not 'search' since it causes a bunch of excess checking + for _,part in pairs(part:GetConnectedParts(true)) do + -- If not found, add it, otherwise continue + if not searchedParts[part] then + add(part) + else continue end + -- If the part is connected to a Character + local joints = part:GetJoints() + for _,joint in pairs(joints) do + if joint:IsA("HingeConstraint") or joint:IsA("PrismaticConstraint") then + -- Also check joints since their network ownership is funky otherwise + if joint.Attachment0 and joint.Attachment1 then + search(joint.Attachment0.Parent) + search(joint.Attachment1.Parent) + end + end + end + end + end + search(instance) + return objectsFound,playersSeated,assemblyRoots +end + +-- Out of a list of player-seats get the highest priority player seated (nil == server) +local function maxPlayerSeated(playersSeated) + local maxPlayer,maxSeat = nil + for player,seat in pairs(playersSeated) do + if maxRankSeat(maxSeat,seat) then maxPlayer = player maxSeat = seat end + end + return maxPlayer +end + +local function mark(object) + if object:FindFirstChild("Mark") then object.Mark:Destroy() end + local box = Instance.new("SelectionBox") + box.Parent = object + box.Adornee = object + box.Name = "Mark" + return box +end + +-- This sort of runs on every client, so maybe it's far too slow? +processOwnership = function(seat) + local objectsFound,playersSeated,assemblyRoots = getParts(seat) + local ownerPlayer = maxPlayerSeated(playersSeated) + -- The server needs to change the network owner in this case + if _G.IsServer then + _G.Remotes.OwnershipReplicate:FireAllClients(seat) + for _,root in pairs(assemblyRoots) do + local success,result = pcall(function() + root:SetNetworkOwner(ownerPlayer) + end) + if not success then warn(result) end + end + for object in pairs(objectsFound) do + local data = objects[object] + if data then data.owner = ownerPlayer end + end + if ownerPlayer then + for object in pairs(objectsFound) do + sleep(object) + end + end + end + if ownerPlayer == thisPlayer then + for _,object in pairs(objectsFound) do -- Connected and Owning + --if _G.IsClient then mark(object).Color3 = Color3.fromRGB(0,255,0) end + own(object) + end + elseif playersSeated[thisPlayer] then -- Connected and not Owning + for _,object in pairs(objects) do + if objectsFound[object] then + --if _G.IsClient then mark(object).Color3 = Color3.fromRGB(255,0,0) end + sleep(object) + end + end + elseif _G.IsClient then -- Disconnected and not Owning + for object,data in pairs(objects) do + if objectsFound[object] then + --if _G.IsClient then mark(object).Color3 = Color3.fromRGB(0,0,255) end + remove(object) + end + end + end + if _G.IsClient then + local ourSeat = playersSeated[thisPlayer] + if ourSeat then + input.seat = ourSeat + context.seat = ourSeat + elseif input.seat == asObject(seat,function(...) return ... end) then + input.seat = nil + context.seat = nil + end + end +end + +local function objectsProcess(delta) + context.delta = delta + context.cursor = input.cursor + for object,data in pairs(owning) do + for _,module in pairs(modules[object.Name] or {}) do + if module.run then if not module.run(data,object,context) then remove(object) end end + end + end + for object,data in pairs(objects) do + for _,module in pairs(modules[object.Name] or {}) do + if module.idle then if not module.idle(data,object,context) then remove(object) end end + end + end + for _,module in pairs(modulesByName) do + if module.loop then + module.loop(context) + end + end +end + +if _G.IsServer then + -- Clients and servers will send this to update information between each other (should be handled by values) + _G.Remotes.ObjectReplicate.OnServerEvent:Connect(function(player,object,name,...) + local data = objects[object] + if data and data.owner == player then + modulesByName[name].replicate(data,object,context,...) + end + end) + game.Players.PlayerRemoving:Connect(function(player) + for object,data in pairs(objects) do + if data.owner == player then + own(object) + end + end + end) +elseif _G.IsClient then + -- Respond to processing events from the server + _G.Remotes.OwnershipReplicate.OnClientEvent:Connect(function(seat) + processOwnership(seat) + end) +end + +for _,instance in pairs(script.Parent.Objects:GetChildren()) do + local module = require(instance) + modulesByName[instance.Name] = module + module.name = instance.Name + module.next_replication = tick() + module.replication_delta = module.replication_delta or 0.5 + if module.manifest.predicate then + predicates[module] = module.manifest.predicate + else + for _,name in pairs(module.manifest) do + modules[name] = modules[name] or {} + table.insert(modules[name],module) + end + end +end + +if _G.IsServer then + for _,instance in pairs(game.Workspace.Objects:GetChildren()) do + add(instance) + end +end + +spawn(function() + _G.Run.Heartbeat:Connect(function(delta) + if _G.IsClient then context.controls = input.values end + objectsProcess(delta) + end) +end) + +return {processOwnership = processOwnership, add = add, update = update, own = own} \ No newline at end of file diff --git a/scripts/modules/PaintModule.lua b/scripts/modules/PaintModule.lua new file mode 100644 index 0000000..3ae9066 --- /dev/null +++ b/scripts/modules/PaintModule.lua @@ -0,0 +1,242 @@ +require(game.ReplicatedFirst.ReadyModule) +-- This is a script that handles a tool for creating objects +local player = game.Players.LocalPlayer +-- Gui Objects +local paintFrame = _G:WaitFor("PaintFrame") +--local paintSelection = paintFrame:WaitForChild("ScrollingFrame") +--local paintDefault = paintSelection:WaitForChild("Default") +local userInputEvent = nil + +local renderEvent = nil +local mouseModule = require(_G.Modules.MouseModule) + +local currentColor = Color3.new(1, 1, 1) +local lastPainted = nil + +local input = game:GetService("UserInputService") + +local partStateModule = require(_G.Modules.PartStateModule) +local partState = partStateModule.new() + +local paintFrame = _G:WaitFor("PaintFrame") + +local colorPickerFrame = paintFrame.ColorPickerFrame +local colorSelectorFrame = paintFrame.ColorSelectorFrame +local colorLabel = paintFrame.ColorLabel + +local materialSelectorFrame = paintFrame.MaterialSelectorFrame +local materialLabel = paintFrame.MaterialLabel + +local polyFrame = paintFrame.PolyFrame +local polyLabel = paintFrame.PolyLabel + +-- local label = game:GetService("Selection"):Get()[1] for name,id in ([[DiamondPlate7546654401Metal7547178395Wood7547190453WoodPlanks7547301709SmoothPlastic Rubber14108673018Neon Glass7547304577]]):gmatch("([a-zA-Z]+)([0-9]*)") do local l = label:Clone() l.Parent = label.Parent l.Image = "rbxassetid://"..id l.Name = name end + +local hues = {} +local hue = Color3.new(0,0,1) +local finalColor = nil + +local function makeColorGradient() + local n = tonumber + local t = {} + for r,g,b in ("100101001011010110100"):gmatch("(.)(.)(.)") do + local color = Color3.new(n(r),n(g),n(b)) + --table.insert(t,ColorSequenceKeypoint.new(#t/7,color)) + table.insert(hues,color) + end + --return ColorSequence.new(t) +end +makeColorGradient() + +local function getHue(value) + local index1 = math.clamp(math.floor(value*6)+1,1,7) + local index2 = math.clamp(math.floor(value*6)+2,2,7) + local a = hues[index1] + local b = hues[index2] + local alpha = value*6 - math.floor(value*6) + print(value,index1,index2,alpha) + return a:Lerp(b,alpha) +end + +local function pickerFactory(object,callback,use_x,use_y) + local held + local inputs = {[Enum.UserInputType.MouseButton1]=true,[Enum.UserInputType.Touch]=true,[Enum.UserInputType.MouseMovement]=true} + local function changed(input) + if input == held or (held and input.UserInputType == Enum.UserInputType.MouseMovement) then + local p = object.Pointer.Position + local s = object.Pointer.AbsoluteSize + local x_scale,y_scale,x_offset,y_offset,x_value,y_value + if use_x then + x_scale = math.clamp((input.Position.X - object.AbsolutePosition.X) / (object.AbsoluteSize.X + s.X),0,object.AbsoluteSize.X/(object.AbsoluteSize.X + s.X)) + x_value = math.clamp((input.Position.X - object.AbsolutePosition.X) / (object.AbsoluteSize.X),0,1) + x_offset = s.X/2 + else + x_scale = p.X.Scale + x_offset = p.X.Offset + end + if use_y then + y_scale = math.clamp((input.Position.Y - object.AbsolutePosition.Y) / (object.AbsoluteSize.Y + s.Y),0,object.AbsoluteSize.Y/(object.AbsoluteSize.Y + s.Y)) + y_value = math.clamp((input.Position.Y - object.AbsolutePosition.Y) / (object.AbsoluteSize.Y),0,1) + y_offset = s.Y/2 + else + y_scale = p.Y.Scale + y_offset = p.Y.Offset + end + object.Pointer.Position = UDim2.new(x_scale,x_offset,y_scale,y_offset) + callback(x_value,y_value) + end + end + object.InputBegan:Connect(function(input) + if input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.MouseButton1 then + held = input + changed(input) + end + end) + object.InputEnded:Connect(function(input) + if input.UserInputType == Enum.UserInputType.Touch or input.UserInputType == Enum.UserInputType.MouseButton1 then + held = nil + end + end) + input.InputChanged:Connect(changed) +end + +local saturation = 0 +local brightness = 0 +local function updateColor() + finalColor = hue:Lerp(Color3.new(1,1,1),saturation):Lerp(Color3.new(0,0,0),brightness) + colorSelectorFrame.Preview.BackgroundColor3 = finalColor + colorLabel.CancelButton.Visible = true +end + +pickerFactory(colorPickerFrame.Hue,function(_,y) + local color = getHue(y) + hue = color + colorPickerFrame.Picker.BackgroundColor3 = color + updateColor() +end,false,true) + +pickerFactory(colorPickerFrame.Picker,function(x,y) + saturation = 1-x + brightness = y + updateColor() +end,true,true) + +local colorPicking = false +colorSelectorFrame.PickButton.Activated:Connect(function() + colorPicking = true + colorSelectorFrame.PickButton.BackgroundColor3 = Color3.new(0.5,1,0.5) +end) + +colorLabel.CancelButton.Activated:Connect(function() + colorLabel.CancelButton.Visible = false + finalColor = nil +end) + +local finalMaterial = nil +local materialStroke = materialSelectorFrame:WaitForChild("UIStroke") +materialStroke.Enabled = true +materialStroke.Parent = nil + +for _,item in pairs(materialSelectorFrame:GetChildren()) do + if item:IsA("ImageButton") then + item.Activated:Connect(function() + finalMaterial = Enum.Material[item.Name] + materialLabel.CancelButton.Visible = true + materialStroke.Parent = item + end) + end +end +materialLabel.CancelButton.Activated:Connect(function() + materialLabel.CancelButton.Visible = false + materialStroke.Parent = nil + finalMaterial = nil +end) + +local finalShift = nil +local finalThickness = nil + +polyFrame.ShiftButton.Activated:Connect(function() + if not finalShift then finalShift = 1 + elseif finalShift == 1 then finalShift = -1 + elseif finalShift == -1 then finalShift = nil end + + if not finalShift then polyFrame.ShiftButton.Text = "Shift Off" + elseif finalShift == -1 then polyFrame.ShiftButton.Text = "Shift Out" + elseif finalShift == 1 then polyFrame.ShiftButton.Text = "Shift In" end + + polyLabel.CancelButton.Visible = true +end) + +polyFrame.ThicknessBox.FocusLost:Connect(function(enter) + if enter then + finalThickness = tonumber(polyFrame.ThicknessBox.Text) or nil + if finalThickness < 0.05 then finalThickness = 0.05 end + local text = "" + if finalThickness then text = tostring(finalThickness).." studs" end + polyFrame.ThicknessBox.Text = text + end + + polyLabel.CancelButton.Visible = true +end) + +polyLabel.CancelButton.Activated:Connect(function() + polyLabel.CancelButton.Visible = false + finalShift = nil + finalThickness = nil + polyFrame.ShiftButton.Text = "Shift Off" + polyFrame.ThicknessBox.Text = "" +end) + +polyLabel.CancelButton.Visible = false +colorLabel.CancelButton.Visible = false +materialLabel.CancelButton.Visible = false + +local function render(delta) + local result = mouseModule.getMouseHit() + if result and result.Instance then + local object = _G.Methods.getObjectFromPart(result.Instance,player) + if object then script.SelectionBox.Adornee = object else script.SelectionBox.Adornee = nil end + script.SelectionBox.FillTransparency = 1 + if not mouseModule.isMouseDown() then return end + script.SelectionBox.FillTransparency = 0.5 + if colorPicking then + finalColor = result.Instance.Color + colorSelectorFrame.Preview.BackgroundColor3 = finalColor + colorPicking = false + colorSelectorFrame.PickButton.BackgroundColor3 = Color3.new(1,1,1) + if object then lastPainted = object end + return + end + if lastPainted == object then return end + lastPainted = object + if not lastPainted then return end + local state = partStateModule.new() + if finalColor then state.color = finalColor end + if finalShift then state.look = game.Workspace.CurrentCamera.CFrame.LookVector state.shift = finalShift end + if finalThickness then state.size = Vector3.new(finalThickness,0,0) end + if finalMaterial then state.material = finalMaterial end + _G.Remotes.Apply:FireServer(lastPainted,state:serialise()) + else + lastPainted = nil + script.SelectionBox.Adornee = nil + end +end + +paintFrame.Visible = false + +local function init(s) + if renderEvent then renderEvent:Disconnect() end + s.Parent.Equipped:Connect(function() + if renderEvent then renderEvent:Disconnect() end + renderEvent = game:GetService("RunService").RenderStepped:Connect(render) + paintFrame.Visible = true + end) + + s.Parent.Unequipped:Connect(function() + if renderEvent then renderEvent:Disconnect() end + paintFrame.Visible = false + script.SelectionBox.Adornee = nil + end) +end + +return {init = init} \ No newline at end of file diff --git a/scripts/modules/PartActivationModule.lua b/scripts/modules/PartActivationModule.lua new file mode 100644 index 0000000..1c202ca --- /dev/null +++ b/scripts/modules/PartActivationModule.lua @@ -0,0 +1,14 @@ +-- module which serves the purpose of providing a universal function so that I can change how checks for whether a player is allowed to activate something works (such as collaborative building) +local module = {} +module.tagService = _G.Tags + +function module.activator(realScript,callback) + local clickDetector = realScript.Parent:FindFirstChildWhichIsA("ClickDetector") + clickDetector.MouseClick:Connect(function(player) + if module.tagService:HasTag(realScript.Parent,"P_"..player.UserId) or module.tagService:HasTag(realScript.Parent.Parent,"P_"..player.UserId) then + callback(player) + end + end) +end + +return module diff --git a/scripts/modules/PartStateModule.lua b/scripts/modules/PartStateModule.lua new file mode 100644 index 0000000..2ed5666 --- /dev/null +++ b/scripts/modules/PartStateModule.lua @@ -0,0 +1,508 @@ +local module = {} + +-- PARTS TABLE +local parts = {} +_G.Storage:WaitForChild("Parts") +local function added(object) + if object.Parent:IsA("Folder") then + local id = object:GetAttribute("PartId") + if id then parts[id] = object end + end +end +for _,object in pairs(_G.Storage.Parts:GetDescendants()) do + added(object) +end +_G.Storage.Parts.DescendantAdded:Connect(function(object) + if object:GetAttribute("PartId") then + warn(string.format("%i:%s, you are late!",object:GetAttribute("PartId"),object:GetFullName())) + end + added(object) +end) +function module.get_template_part(part) + local id = part:GetAttribute("PartId") + if not id then return end + local template = parts[id] + return template +end +function module.get_part_by_id(id) + return parts[id] +end + +function module.change_material(part,material) + if part.Material == Enum.Material.Glass then + part.Transparency = part:GetAttribute("OriginalTransparency",part.Transparency) + end + if material == Enum.Material.Glass then + part:SetAttribute("OriginalTransparency",part.Transparency) + part.Transparency = 0.5 + end + part.Material = material +end + +-- PART METHODS +function module.apply_material(part,material) + if material then + part:SetAttribute("Material",material.Name) + if part:IsA("Model") then + for _,subpart in pairs(part:GetDescendants()) do + if subpart:IsA("BasePart") then + module.change_material(subpart,material) + end + end + else + module.change_material(part,material) + end + end +end +function module.apply_edit(part,index,value) + part[index].Value = value +end +function module.get_size(part) + local nodes = module.get_nodes_class(part) + if nodes == 1 then + return part:GetAttribute("Size") + elseif nodes == 2 then + return Vector3.new(part.Size.Z,0,0) -- Yeah the bloody irony I know right? + elseif nodes >= 3 then + return Vector3.new(part:FindFirstChildWhichIsA("Part").Size.X,0,0) + end +end +function module.get_thickness(part) + return module.get_size(part).X -- WHAT?! +end +function module.apply_size(part,size) + part:SetAttribute("Size",size) + local nodes = module.get_nodes_class(part) + if nodes == 1 then + if part:IsA("Model") then + part:ScaleTo(size.X) + elseif part:IsA("BasePart") then + if size.Y == 0 or size.Z == 0 then return end -- Since this is not for normal parts it's a poly size + part.Size = size + end + elseif nodes == 2 then + part.Size = Vector3.new(part.Size.X,size.X,size.X) -- Yeah, this is serious + elseif nodes >= 3 then + for _,item in pairs(part:GetChildren()) do + item.Size = Vector3.new(size.X,item.Size.Y,item.Size.Z) + end + end +end +function module.apply_color(part,color) + part:SetAttribute("Color",color) + if part:IsA("Model") then + for _,subpart in pairs(part:GetDescendants()) do + if subpart:IsA("BasePart") then + subpart.Color = color + end + end + else + part.Color = color + end +end +local function Weld(part1,part2,temporary) -- Make a BUILDING weld + local weld = Instance.new("WeldConstraint") + weld.Name = "BuiltWeldConstraint" + weld.Parent = part1 + weld.Part0 = part1 + weld.Part1 = part2 + if temporary then weld.Name = "WeldConstraint" end -- Or not. + return weld +end +function module.weld(part,connection,temporary) + local attach = part + if part:IsA("Model") then attach = attach.PrimaryPart end + local new_weld = Weld(attach,connection,temporary) + new_weld.Parent = part + return new_weld +end +function module.form_triangle(pa,pb,p1,p2,p3,s2) + local s1 = pa.Size.X + s2 = s2 or s1*0.5 + s2 = 0 + local p12 = p2-p1 + local p23 = p3-p2 + local p13 = p3-p1 + local p12l = p12.Magnitude + local p23l = p23.Magnitude + local p13l = p13.Magnitude + local bottom + local bot_len + local totop + local root + local tip + local far + local t + if p12l > p23l and p12l > p13l then -- TRUE: p12 largest FALSE: p12 smaller than p23l or p13l + tip = p3 + root = p1 + far = p2 + bottom = p12 + bot_len = p12l + totop = p13 + --[[pa.Color = Color3.new(1,0,0) + pb.Color = Color3.new(1,0,0)]] + elseif p23l > p13l then -- TRUE: p23 must be largest FALSE: p13 largest + tip = p1 + root = p2 + far = p3 + bottom = p23 + bot_len = p23l + totop = -p12 + --[[pa.Color = Color3.new(0,1,0) + pb.Color = Color3.new(0,1,0)]] + else -- p13 largest + tip = p2 + root = p1 + far = p3 + bottom = p13 + bot_len = p13l + t = pa + pa = pb + pb = t + --[[pa.Color = Color3.new(0,0,1) + pb.Color = Color3.new(0,0,1)]] + totop = p12 + end + bottom = bottom.Unit + local coeff = totop:Dot(bottom) + local mid = root+coeff*bottom + local cross = totop:Cross(bottom).Unit + local top = (mid-tip) + pa.Size = Vector3.new(s1,top.Magnitude,coeff) + pb.Size = Vector3.new(s1,top.Magnitude,bot_len - coeff) + top = top.Unit + pa.CFrame = CFrame.fromMatrix(s2*cross+(root+tip)*0.5,cross,-top,bottom) + pb.CFrame = CFrame.fromMatrix(s2*cross+(tip+far)*0.5,-cross,-top,-bottom) +end +function module.get_tri_positions1(pa,pb) + local ca = pa.CFrame + local ra = ca.Rotation + local cb = pb.CFrame.Position + local sa = pa.Size + local lb = pb.Size.Z + local lh = sa.Y + local fb = Vector3.new(0,lh,-lb) + return (cb - ra * (fb * 0.5)) -- FOR THIRD RETURN VALUE OF BELOW +end +function module.get_tri_positions2(pa,pb) + local ca = pa.CFrame + local ra = ca.Rotation + ca = ca.Position + local sa = pa.Size + local la = sa.Z + local lh = sa.Y + local fa = Vector3.new(0,lh,la) + return (ca + ra * (fa * -0.5)),(ca + ra * (fa * 0.5)) +end +-- apply data positions of parts +local form_triangle = module.form_triangle +local number_to_string = {"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"} +local string_to_number = {} +for num,str in pairs(number_to_string) do + string_to_number[str] = num +end +--; for i = 1,10 do number_to_string[i] = tostring(i) end +local mod = CFrame.Angles(0,0,0) +function module.apply_nodes(part,nodes) + if #nodes < module.get_nodes_class(part) then return true end + if #nodes == 1 then + part:PivotTo(nodes[1]) + elseif #nodes == 2 then + part.CFrame = nodes[1]*mod + part.Size = Vector3.new(nodes[2],part.Size.Y,part.Size.Z) + local first = part:FindFirstChild("1") + local second = part:FindFirstChild("2") + local p1,p2 = module.get_stick_ends(part) + local offset = Vector3.new(nodes[2]*0.5,0,0) + if first then + first.Position = nodes[1]:PointToWorldSpace(offset) + if first:FindFirstChild("WeldConstraint") then + first.WeldConstraint.Enabled = true + end + end + if second then + second.Position = nodes[1]:PointToWorldSpace(-offset) + if second:FindFirstChild("WeldConstraint") then + second.WeldConstraint.Enabled = true + end + end + elseif #nodes >= 3 then + local template = part["A"] + for i=1,(#nodes-2)*2 do + local name = number_to_string[i] + if not part:FindFirstChild(name) then + local piece = template:Clone() + piece.Parent = part + piece.Name = name + end + end + for i=1,(#nodes-2) do + form_triangle(part[number_to_string[i*2-1]],part[number_to_string[i*2]],nodes[1],nodes[i+1],nodes[i+2]) + end + end +end +-- get the poly 'type' of a part +function module.get_nodes_class(part) + return part:GetAttribute("Nodes") or 1 +end +-- get data positions of parts +function module.get_nodes(part) + local nodes = part:GetAttribute("Nodes") + if not nodes then + return {part:GetPivot()} + elseif nodes == 2 then + local first = part:FindFirstChild("1") + local second = part:FindFirstChild("2") + if first and second then + local offset = second.Position - first.Position + return {CFrame.lookAt(first.Position,second.Position,first.CFrame.UpVector)*CFrame.Angles(0,math.pi*0.5,0) + offset * 0.5,offset.Magnitude} + else + return {part.CFrame,part.Size.X} + end + elseif nodes >= 3 then + local i = -1 + local p1 = part["A"] + local p2 = part["B"] + local t = {module.get_tri_positions2(p1,p2)} + while true do + i = i + 2 + local pn1 = part:FindFirstChild(number_to_string[i]) + if not pn1 then return t end + local pn2 = part:FindFirstChild(number_to_string[i+1]) + if not pn2 then return t end + table.insert(t,module.get_tri_positions1(p1,p2)) + end + return t + end +end +-- shift a poly part along its normal axis while keeping static node positions +function module.apply_shift(part,shift) + if module.get_nodes_class(part) < 3 then return end + local previous = part:GetAttribute("Shift") or 0 + if shift == 0 and previous == 0 then part:SetAttribute("Shift",nil) return end + part:SetAttribute("Shift",shift) + shift = shift - previous + for _,item in pairs(part:GetChildren()) do + if item:IsA("BasePart") and not item:HasTag("Node") then + local neg = (string_to_number[item.Name] % 2 == 0) + if neg then neg = -1 else neg = 1 end + item.Position = item.Position + item.CFrame.RightVector * item.Size.X * 0.5 * shift * neg + end + end +end +-- stupid name, it just tries to shift in the direction based on how we're looking +function module.attempt_shift(part,shift,look) + local template = part:FindFirstChild("A") + if not template then return end + module.apply_shift(part,math.sign(template.CFrame.RightVector:Dot(look)) * shift) +end +-- get the cframe of a single connection +function module.eval_connection(connection) + local connected = connection.connected + local relative = CFrame.identity + if connected then relative = connected:GetPivot() end + return relative:ToWorldSpace(connection.CFrame) +end +function module.get_stick_ends(part) + local nodes = module.get_nodes(part) + if #nodes ~= 2 then error("Part is not a stick",2) end + local cframe = nodes[1] + local offset = Vector3.new(nodes[2]*0.5,0,0) + return cframe:PointToWorldSpace(offset),cframe:PointToWorldSpace(-offset) +end +function module.get_points(part) + local class = module.get_nodes_class(part) + if class == 1 then return {} + elseif class == 2 then return {module.get_stick_ends(part)} + else return module.get_nodes(part) end +end +-- apply driven connections by attachments in the placement engine +function module.eval_connections(part,connections) + local class = module.get_nodes_class(part) + local cframes = {} + for _,connection in ipairs(connections) do + table.insert(cframes,module.eval_connection(connection)) + end + local nodes = {} + if class == 1 then + nodes = cframes + elseif class == 2 then + if #cframes == 2 then + local to = cframes[2].Position - cframes[1].Position + nodes = {CFrame.lookAt(cframes[1].Position,cframes[2].Position,cframes[1].UpVector)*CFrame.Angles(0,math.pi*0.5,0) + (to * 0.5), to.Magnitude} + end + elseif class == 3 then + for _,cframe in ipairs(cframes) do table.insert(nodes,cframe.Position) end + end + return nodes +end + +function module.apply_internal_welds(part) + if part:IsA("Model") then + local main = part.PrimaryPart + for _,item in pairs(part:GetChildren()) do + if item:IsA("BasePart") and item ~= main then Weld(item,main,true) end + end + end +end + +-- PART STATE METHODS +local _state = {} +_state.__index = _state +function _state.apply_material(this) + if this.material then module.apply_material(this.part,this.material) end +end +function _state.apply_size(this) + if this.size then module.apply_size(this.part,this.size) end +end +function _state.apply_color(this) + if this.color then module.apply_color(this.part,this.color) end +end +function _state.apply_edit(this,index,value) + local edit = this.part:FindFirstChild(index) + if not edit then error("Could not find edit "..index,2) end + module.apply_edit(this.part,index,value) +end +function _state.apply_edits(this) + for index,value in pairs(this.edits) do + this:apply_edit(index,value) + end +end +function _state.attempt_shift(this,...) + module.attempt_shift(this.part,...) +end +function _state.apply_internal_welds(this) + if module.get_nodes_class(this.part) > 1 then -- Weld together parts inside of poly + module.apply_internal_welds(this.part) + end +end +function _state.apply_welds(this) + this:apply_internal_welds() + for index,connection in pairs(this.welds) do + local target = this.part + if connection.name then target = this.part[connection.name] end + if target:IsA("Model") then target = target.PrimaryPart end + if (not this.owner) or (_G.Methods.getOwnedOrNil(this.owner,connection.part)) then + module.weld(target,connection.part) + end + end +end +function _state.apply_connections(this) + this.nodes = module.eval_connections(this.part,this.connections) +end +function _state.apply_nodes(this) + module.apply_nodes(this.part,this.nodes) +end +function _state.apply_shift(this) + if this.look then this:attempt_shift(this.shift or 0,this.look) + else module.apply_shift(this.part,this.shift or 0) end +end +function _state.apply(this,part,options) + options = options or {} + this.part = part or this.part + if this.color then this:apply_color() end + if this.size then this:apply_size() end + if this.material then this:apply_material() end + if this.edits and not options.no_edit then this:apply_edits() end + if this.connections then this:apply_connections() end + if this.nodes then this:apply_nodes() end + if this.welds and not options.no_weld then this:apply_welds() else this:apply_internal_welds() end + if this.shift then this:apply_shift() end +end +function _state.create(this) + if not this.id then error("No id",2) end + local template = parts[this.id]:Clone() + template.Parent = game.Workspace.Objects + this:apply(template) + return template +end +function _state.get_nodes_class(this) + return module.get_nodes_class(this.part) +end +function _state.take(this,existing) + this.color = existing:GetAttribute("Color") + this.material = existing:GetAttribute("Material") + this.size = existing:GetAttribute("Size") + this.shift = existing:GetAttribute("Shift") + this.edits = {} + for _,edit in existing:GetChildren() do + if edit:HasTag("Edit") then + if edit.Value ~= (edit:GetAttribute("Default") or nil) then + this.edits[edit.Name] = edit.Value + end + end + end + this.nodes = module.get_nodes(existing) + return this +end +function _state.update(this) + this:take(this.part) + return this +end +function _state.copy(this,existing) + this:take(existing) + this.id = existing:GetAttribute("PartId") + this.part = parts[this.id]:Clone() + this:apply(nil,{no_weld = true}) + return this +end +function module.copy(existing) + return module.new():copy(existing) +end +function module.new(original) + return setmetatable(_G.Union({ + material = nil, -- Enum.Material SERIAL + color = nil, -- Color3.new() SERIAL + size = nil, -- Vector3.new() SERIAL + shift = nil, -- 1 -> 0 -> -1 SERIAL + connections = {}, -- {[n]={cframe=,connection=}} for placement engine etc + welds = {}, -- {{name=,touched=},...},... SERIAL + edits = {}, -- ? SERIAL + owner = nil, -- player + part = nil, -- part being worked with (applied) + id = nil -- part id SERIAL + },original or {}),_state) +end + +-- SERIALISATION +--[[local function serialisation_factory(match_string) + local serialisation_table = {} + for index in match_string:gmatch("[^,]+") do + table.insert(serialisation_table,index) + end + return { + serialise = function(this) + local t = {} + for _,key in pairs(serialisation_table) do + table.insert(t,this[key]) + end + return table.unpack(t) + end, + deserialise = function(this,...) + local t = {...} + for index,key in pairs(serialisation_table) do + this[key] = t[index] + end + end, + } +end +local state_serialiser = serialisation_factory("id,connections,color,size,shift,material,edits,welds")]] +_state.serialise = function(this) --state_serialiser.serialise + local color + if this.color then color = {this.color.R,this.color.G,this.color.B} end + return this.id,this.connections,color,this.size,this.shift,this.material,this.edits,this.welds,this.look +end +_state.deserialise = function(this,id,connections,color,size,shift,material,edits,welds,look) --state_serialiser.deserialise + if color then this.color = Color3.new(table.unpack(color)) end + this.id = id + this.connections = connections + this.size = size + this.shift = shift + this.material = material + this.edits = edits + this.welds = welds + this.look = look +end + +return module \ No newline at end of file diff --git a/scripts/modules/PlacementModule.lua b/scripts/modules/PlacementModule.lua new file mode 100644 index 0000000..991e20d --- /dev/null +++ b/scripts/modules/PlacementModule.lua @@ -0,0 +1,627 @@ +local run = game:GetService("RunService") +local inputService = game:GetService("UserInputService") +local collectionService = _G.Tags +local mouseModule = require(_G.Modules.MouseModule) +local methodModule = require(_G.Modules.MethodModule) +local partStateModule = require(_G.Modules.PartStateModule) +local selectionBox = script.SelectionBox +local adjacentBox = script.AdjacentBox +local statGui +if game.Players.LocalPlayer then + statGui = game.Players.LocalPlayer:WaitForChild("PlayerGui"):WaitForChild("StatGui") +end +local gui +local current = nil + +local mode = "place" +local realMode = "place" +local rotSnap = 15 +local rotSnapKey = 45. +local moveSnap = 0.5 +local modelScaleSnap = 0.1 +local touchModeConnect = true +local touchingBoxes = {} + +local controlModifier = false -- embed +local shiftModifier = false -- lower snap + +local ModeButtons = { + clone = nil, + resize = nil +} + +local pointer = nil + +local function temp() + local a = Instance.new("Part") + a.Parent = game.Workspace + a.Size = Vector3.new(1,1,1) + a.Anchored = true + a.CanCollide = false + a.Transparency = 0.5 + a.Material = Enum.Material.Neon + return a +end + +local a = temp() +local b = temp() + +local modeEnd = function() + if current.axes then current.axes:Destroy() end +end +local focusAxes = function() + current.axes:ScaleTo(current.size.Magnitude*1.3) + methodModule.setItemCFrame(current.axes,current.cframe) + for _,part in pairs(current.axes:GetChildren()) do + if not current.axis then + part.Transparency = 0.5 + else + part.Transparency = 1 + end + end + if current.axis then current.axis.Transparency = 0 end +end +local createAxes = function(name) + current.axes = _G.Storage.Assets[name]:Clone() + current.axes.Parent = game.Workspace + current.axes.PrimaryPart.Shape = Enum.PartType.Ball + focusAxes() +end +local getBounds = function() + local cframe,size + if current.model:IsA("Model") then + cframe,size = current.model:GetBoundingBox() + else + cframe = current.model.CFrame + size = current.model.size + end + current.cframe = cframe + current.size = size +end +local styleModel = function() + local function style(part) + part.Anchored = true + part.CanCollide = false + end + if current.model:IsA("BasePart") then style(current.model) end + for _,part in pairs(current.model:GetChildren()) do + if part:IsA("BasePart") then + style(part) + end + end +end +local distanceAlong = function(o : Vector3,n : Vector3,p : Vector3) -- origin, normal, point + local k = (n.X*(p.X-o.X)+n.Y*(p.Y-o.Y)+n.Z*(p.Z-o.Z))--/(n.x*n.x+n.y*n.y+n.z*n.z) -- is 1 + return (n*k+o-p).Magnitude +end +local processPointer = function(inputObject) + local position = inputObject.Position + local result,ray = mouseModule.getMouseHit() -- get whatever we're hovering + current.location = position + current.ray = ray + current.result = result +end + +local function axisClick(inputObject) -- For whenever we click in a mode supporting axis + if current.axes then -- if there are axes (there should be) + processPointer(inputObject) + if current.result then + if current.result.Instance.Parent == current.axes then -- if it's an axis + current.axis = current.result.Instance -- set it + pointer = inputObject -- set the inputObject for current selection + end + end + end +end + +local function round(v : Vector3,snap) -- just round a vector based on the snapping + snap = snap or moveSnap + return Vector3.new(math.round(v.X/snap)*snap,math.round(v.Y/snap)*snap,math.round(v.Z/snap)*snap) +end + +local function mask(a : Vector3,b : Vector3) -- multiply each axis together + return Vector3.new(a.X*b.X,a.Y*b.Y,a.Z*b.Z) +end + +local function clamp(a : Vector3, min : Vector3, max : Vector3) + return Vector3.new(math.clamp(a.X,min.X,max.X),math.clamp(a.Y,min.Y,max.Y),math.clamp(a.Z,min.Z,max.Z)) +end + +local function rayProjectToRay(ray,axis,root) + local n2 = ray.Direction + local o2 = ray.Origin + local n1 = (axis.Position - axis.Parent.PrimaryPart.Position).Unit + local o1 = root or current.axis.Position + -- using more vector magic to find the closest point on the axis line to the ray of the pointer + local s = n2:Dot(n1) + return o1+n1*((n1:Dot(o2-o1)+s*(n2:Dot(o1-o2)))/(1-s*s)) +end + +local function axisDragClick(inputObject) + axisClick(inputObject) + if current.axis then + local p = rayProjectToRay(current.ray,current.axis) + current.modelOrigin = current.touchingCFrame + current.dragOrigin = current.axes.PrimaryPart.CFrame:PointToObjectSpace(p) + current.axisOrigin = current.touching.CFrame:ToObjectSpace(current.axis.CFrame) + end +end + +local function rayProjectToPlane(ray,axis) + -- using some vector magic to find the point of intersection between the ray of the pointer and the plane of rotation (arc handle plane) + return ray.Origin+(axis.CFrame.RightVector:Dot(axis.Position-ray.Origin))/(axis.CFrame.RightVector:Dot(ray.Direction))*ray.Direction +end + +local function axisRotateClick(inputObject) + axisClick(inputObject) + if current.axis then + local p = rayProjectToPlane(current.ray,current.axis) + current.dragOrigin = current.axis.CFrame:PointToObjectSpace(p) + current.modelOrigin = current.touchingCFrame + current.axisOrigin = current.touching.CFrame:ToObjectSpace(current.axis.CFrame) + current.initialRotation = current.rotation + end +end + +local modeStart = {} +modeStart.clone = function() + modeEnd() +end +modeStart.rotate = function() + modeEnd() + createAxes("RotateAxes") +end +modeStart.resize = function() + modeEnd() + createAxes("ResizeAxes") + for _,item in pairs(current.axes:GetChildren()) do + local axis = item.Name:match("Part(.)") + if axis then + if (current.scaleTerms or Vector3.one)[axis] == 0 then axis:Destroy() end -- if the scale terms say that this axis should not be scaled + if current.model:IsA("Model") then item.Color = Color3.fromRGB(13, 105, 172) end + end + end +end +modeStart.move = function() + modeEnd() + createAxes("MoveAxes") +end +modeStart.place = function() + modeEnd() +end + +local function setMode(newMode) + for _,other_button in pairs(_G.PlaceModeFrame:GetChildren()) do + if other_button:IsA("TextButton") then + other_button.BackgroundColor3 = Color3.new(1,1,1) + if (current.disabled or {})[other_button.Name] then other_button.Visible = false else other_button.Visible = true end + end + end + realMode = newMode + mode = newMode + gui.Confirm.Text = "Place" + if not current.touching and newMode ~= "place" then + mode = "place" + gui.Confirm.Text = "Confirm" + end + _G.PlaceModeFrame[realMode].BackgroundColor3 = Color3.new(1, 0.956863, 0.345098) + _G.PlaceModeFrame[mode].BackgroundColor3 = Color3.new(0.6, 1, 0.333333) + modeStart[mode]() +end + +local function confirmPlace(inputObject,forced) + if not current then return end + if realMode ~= mode then setMode(realMode) return end + if (inputObject and inputObject.UserInputType == Enum.UserInputType.MouseButton1) or not inputObject then + current:confirm(forced) + if not current then return end + methodModule.setItemCFrame(current.model,CFrame.new()) + current.touching = nil + current.touchingCFrame = CFrame.new() + setMode(realMode) + end +end + +local modeSnap = { + +} + +local modeClick = {} +modeClick.place = function(inputObject) + confirmPlace(inputObject,false) +end +modeClick.clone = function(inputObject) + setMode("place") +end +modeClick.move = function(inputObject) + axisDragClick(inputObject) + current.moveOffset = current.newMoveOffset or Vector3.zero +end +modeClick.rotate = axisRotateClick +modeClick.resize = function(inputObject) + axisDragClick(inputObject) + current.startSize = current.size + if current.model:IsA("Model") then current.startScale = current.model:GetScale() end +end +local modeLoop = {} +modeLoop.default = function() + if current.touching and current.touching:IsA("BasePart") then + methodModule.setItemCFrame(current.model,current.touching.CFrame:ToWorldSpace(current.touchingCFrame or CFrame.identity)) + end + statGui.Enabled = not not statGui.Adornee + getBounds() +end +modeLoop.clone = function() + current:clone(current.touching) + modeLoop.default() +end +modeLoop.rotate = function() + modeLoop.default() + focusAxes() + if pointer then + processPointer(pointer) + local p = rayProjectToPlane(current.ray,current.axis) + local plane = current.touching.CFrame:ToWorldSpace(current.axisOrigin) + local toDrag = plane.Rotation:PointToWorldSpace(current.dragOrigin) + local toPointer = p-plane.Position + local angle = math.round(toDrag:Angle(toPointer,plane.RightVector)/math.rad(rotSnap))*math.rad(rotSnap) + local modelCFrame = current.touching.CFrame:ToWorldSpace(current.modelOrigin) -- cframe of axes/object in the world + local newCFrame = plane.Rotation:ToWorldSpace(CFrame.Angles(angle,0,0)*plane.Rotation:ToObjectSpace(modelCFrame.Rotation)) + modelCFrame.Position -- put it back into object space + current.rotation = current.initialRotation * newCFrame:ToObjectSpace(modelCFrame):Inverse() + current.touchingCFrame = current.touching.CFrame:ToObjectSpace(newCFrame) -- need to add rotation + statGui.TextLabel.Text = tonumber(math.round(math.deg(angle)*1000)/1000) + statGui.Adornee = current.model + else + statGui.Adornee = nil + end +end +local function psumag(v) + return v.X + v.Y + v.Z +end +modeLoop.resize = function() + modeLoop.default() + focusAxes() + if pointer then + processPointer(pointer) + local origin = current.touching.CFrame:ToWorldSpace(current.modelOrigin) + local start = origin:PointToWorldSpace(current.dragOrigin) -- the start of the scale drag + local diff = (rayProjectToRay(current.ray,current.axis)-start) -- difference between start and end + local scaleChange = origin.Rotation:PointToObjectSpace(diff) + if current.model:IsA("Model") then + local before = mask(current.startSize,current.dragOrigin.Unit) -- the size in this axis before scaling + local after = psumag(mask(scaleChange,current.dragOrigin.Unit)) -- the size in this axis after scaling + local scale = math.round(((((after*math.sign(psumag(before)))/psumag(before))+1)/modelScaleSnap)*current.startScale)*modelScaleSnap + scale = math.clamp(scale,0.5,current.model:GetAttribute("MaxSize") or 3) + current.model:ScaleTo(scale) + current.model:SetAttribute("Size",Vector3.new(scale,0,0)) + current.touchingCFrame = current.modelOrigin + current.modelOrigin.Rotation:PointToWorldSpace(((scale/current.startScale)-1)*before*0.5) + statGui.TextLabel.Text = tonumber(math.round(scale*1000)/1000) + else + local change = round(mask(scaleChange,current.scaleTerms or Vector3.one)) + current.model.Size = clamp(current.startSize + mask(change,current.dragOrigin.Unit),Vector3.one*0.1,current.model:GetAttribute("MaxSize") or Vector3.one*20) + current.model:SetAttribute("Size",current.model.Size) + current.touchingCFrame = current.modelOrigin + current.modelOrigin.Rotation:PointToWorldSpace(change/2) + statGui.TextLabel.Text = tonumber(math.round(mask(current.model.Size,current.dragOrigin.Unit).Magnitude*1000)/1000) + end + statGui.Adornee = current.model + else + statGui.Adornee = nil + end + modeLoop.default() -- the resize system has this weird lag, i cannot be asked fixing it unfortunately +end +modeLoop.move = function() + modeLoop.default() + focusAxes() + if pointer then + processPointer(pointer) + local origin = current.touching.CFrame:ToWorldSpace(current.modelOrigin) + local start = origin:PointToWorldSpace(current.dragOrigin) + local diff = (rayProjectToRay(current.ray,current.axis,start)-start) -- difference between start and end + diff = current.touching.CFrame.Rotation:PointToObjectSpace(diff) + local offset = psumag(mask(current.moveOffset,diff.Unit)) + local len = math.abs(psumag(mask(current.size,origin:ToObjectSpace(current.axis.CFrame).RightVector))) + diff = diff.Unit * math.round(math.clamp(diff.Magnitude,-len*0.5-offset,len*0.5-offset)/moveSnap) * moveSnap + current.touchingCFrame = current.modelOrigin + diff + current.newMoveOffset = current.moveOffset + diff + statGui.TextLabel.Text = tonumber(math.round(diff.Magnitude*1000)/1000) + statGui.Adornee = current.model + else + statGui.Adornee = nil + end +end +modeLoop.place = function() + local result,ray,update = mouseModule.getMouseHit() + if result and result.Instance and update then + current.touching = result.Instance + if current.touching.Locked then adjacentBox.Adornee = nil end + if not current.touching then return end + if not current.touching.Position then print(current.touching) return end + local rotation = (current.touching.CFrame - current.touching.Position) + rotation = rotation * current.rotation + local s = current.size*0.5 -- So half the size of the part, rotated correctly + local max = 0 + local points = {Vector3.new(s.X,s.Y,s.Z),Vector3.new(s.X,-s.Y,s.Z),Vector3.new(-s.X,s.Y,s.Z),Vector3.new(-s.X,-s.Y,s.Z),Vector3.new(s.X,s.Y,-s.Z),Vector3.new(s.X,-s.Y,-s.Z),Vector3.new(-s.X,s.Y,-s.Z),Vector3.new(-s.X,-s.Y,-s.Z)} + for _,point in pairs(points) do + max = math.max(max,result.Normal:Dot(rotation:PointToWorldSpace(point))) + end + local push = max + local snap = moveSnap + if shiftModifier then snap = moveSnap * 0.5 end + local depth = (Ray.new(result.Position,-result.Normal):ClosestPoint(current.touching.Position)-result.Position).Magnitude + local flat = current.touching.CFrame:PointToWorldSpace(round(current.touching.CFrame:PointToObjectSpace(result.Position - result.Normal * depth),snap)) + if controlModifier or current.model:HasTag("Node") then push = 0 end + local cframe = rotation + flat + result.Normal * (depth + push) -- Add the rotation, Snapped movement perpendicular to normal and the Offset/Pushback + if current.model:HasTag("Node") then + local part + if current.touching:GetAttribute("Nodes") then + part = current.touching + elseif current.touching.Parent:GetAttribute("Nodes") then + part = current.touching.Parent + end + if part then + local ideal + local current_distance = 1 + for _,node in ipairs(partStateModule.get_points(part)) do + local distance = (node - result.Position).Magnitude + if distance < current_distance then + current_distance = distance + if part:IsA("Model") then + ideal = part.PrimaryPart + else + ideal = part + end + if current.model:IsA("Model") then + cframe = rotation + node + else + cframe = current.model.CFrame.Rotation + node + end + current.touching = ideal + end + end + end + end + if current.touching:HasTag("Node") then + if current.model:IsA("Model") then + cframe = rotation + current.touching.Position + else + cframe = current.model.CFrame.Rotation + current.touching.Position + end + end + methodModule.setItemCFrame(current.model,cframe) + current.touchingCFrame = current.touching.CFrame:ToObjectSpace(cframe) + current.newMoveOffset = Vector3.zero + adjacentBox.Adornee = current.touching + end + getBounds() +end + +local function render() + if not current then return end + controlModifier = inputService:IsKeyDown(Enum.KeyCode.LeftControl) or inputService:IsKeyDown(Enum.KeyCode.LeftMeta) + shiftModifier = inputService:IsKeyDown(Enum.KeyCode.LeftShift) + modeLoop[mode]() +end + +--[[ +config: + - Can Resize + - Confirm Callback +]] +local placetouchbuttoncolor +local function toggleTouching() + touchModeConnect = not touchModeConnect + if touchModeConnect then + _G.PlaceTouchButton.BackgroundColor3 = placetouchbuttoncolor + else + _G.PlaceTouchButton.BackgroundColor3 = Color3.new(1,1,1) + end +end +function begin(model,config) + if current then current:stop() end + gui.Visible = true + current = config or {} + current.stop = function(config) + if not config then warn("No config passed to PlacementModule; current:stop(). Using current.") config = current end + if not config then return end + if config.axes then config.axes:Destroy() end + if config.stopped then config:stopped() end + current = nil + selectionBox.Adornee = nil + adjacentBox.Adornee = nil + for _,box in pairs(touchingBoxes) do + box:Destroy() + end + touchingBoxes = {} + gui.Visible = false + end + model.Parent = game.Workspace.Hidden + if model:IsA("Model") then + model.PrimaryPart.Anchored = true + else + model.Anchored = true + end + ModeButtons.resize.Visible = config.resize + if not config.resize and mode == "resize" then mode = "place" end + if not config.rotate and mode == "rotate" then mode = "place" end + _G.PlaceTouchButton.Visible = config.touch + selectionBox.Adornee = model + current.model = model + if not current.model then error("urmum") end + current.connections = current.connections or {} + current.rotation = current.model:GetAttribute("Rotation") or CFrame.identity + setMode(realMode) + styleModel() + getBounds() + modeStart[mode]() + return config +end +function init(_gui) + gui = _gui + _G:WaitFor("PlaceModeFrame") _G:WaitFor("PlaceSnapFrame") _G:WaitFor("PlaceCancelButton") + statGui.Enabled = false + placetouchbuttoncolor = _G.PlaceTouchButton.BackgroundColor3 + gui.Visible = false + run:BindToRenderStep("placementmodule",Enum.RenderPriority.Last.Value,render) + local touchOverlapParams = OverlapParams.new() + touchOverlapParams.MaxParts = 3 + touchOverlapParams.FilterType = Enum.RaycastFilterType.Include + touchOverlapParams.FilterDescendantsInstances = {game.Workspace.Objects} + touchOverlapParams.RespectCanCollide = true + touchOverlapParams.Tolerance = 0.05 + run.Heartbeat:Connect(function() -- Touching parts loop + if current and current.model and current.touch then -- in placement + local loop_current = current + local connections = {} + if touchModeConnect then + local items + if current.model:IsA("Model") then -- Check all potential subparts + items = current.model:GetChildren() + elseif current.model:IsA("BasePart") then + items = {current.model} + end + local count = 0 + for _,item in pairs(items) do + if item:IsA("BasePart") then + for _,part in pairs(workspace:GetPartsInPart(item,touchOverlapParams)) do -- Get touching parts + if current.connections[part] then connections[part] = part continue end + local object = _G.Methods.getOwnedOrNil(_G.Player,part) -- Check if this is owned + if object then + connections[object] = true -- Add to the current touching ones + current.connections[object] = { -- Add to the final list + part = object + } + if current.model ~= item then current.connections[object].name = item.Name end + + --if part == current.touching then continue end + local box = object:FindFirstChild("TouchingBox") + if not box then box = script.TouchingBox:Clone() box.Parent = object end + touchingBoxes[object] = box + end + end + end + if count > 3 then wait() count = 0 else count = count + 1 end -- Dangerous! Things can change after this (wait) + if current ~= loop_current then return end -- Current is invalid now so stop! + end + elseif current.touching then + local connection = { + part = current.touching + } + current.connections[current.touching] = connection + connections[current.touching] = connection + end + for index,connection in pairs(current.connections) do -- Remove all invalid connections too + if not connections[index] then + if touchingBoxes[index] then + touchingBoxes[index]:Destroy() + touchingBoxes[index] = nil + end + current.connections[index] = nil + end + end + end + end) + _G.PlaceTouchButton.Activated:Connect(toggleTouching) + inputService.InputBegan:Connect(function(inputObject,irrelevant) + if not current then return end + if irrelevant then return end + local sign = 1 + if shiftModifier then sign = -1 end + if inputObject.UserInputType == Enum.UserInputType.Keyboard then + if inputObject.KeyCode == Enum.KeyCode.R then + if controlModifier then + _G.PlaceSnapInputFrame.RotSnap:CaptureFocus() + else + current.rotation = current.rotation * CFrame.Angles(math.rad(rotSnapKey*sign),0,0) + end + elseif inputObject.KeyCode == Enum.KeyCode.T then + if controlModifier then + _G.PlaceSnapInputFrame.MoveSnap:CaptureFocus() + else + current.rotation = current.rotation * CFrame.Angles(0,math.rad(rotSnapKey*sign),0) + end + elseif inputObject.KeyCode == Enum.KeyCode.Y then + if controlModifier then + _G.PlacePolyInputFrame.PolyThicknessInputBox:CaptureFocus() + else + current.rotation = current.rotation * CFrame.Angles(0,0,math.rad(rotSnapKey*sign)) + end + elseif inputObject.KeyCode == Enum.KeyCode.G and not controlModifier then + current.rotation = CFrame.identity + elseif inputObject.KeyCode == Enum.KeyCode.E then + setMode("place") + elseif inputObject.KeyCode == Enum.KeyCode.Q and current.rotate then + setMode("rotate") + elseif inputObject.KeyCode == Enum.KeyCode.Z and current.resize then + setMode("resize") + elseif inputObject.KeyCode == Enum.KeyCode.X then + setMode("move") + elseif inputObject.KeyCode == Enum.KeyCode.U and current.touch then + toggleTouching() + elseif inputObject.KeyCode == Enum.KeyCode.Backspace then + if current and current.cancel then current:cancel() end + end + elseif inputObject.UserInputType == Enum.UserInputType.MouseButton1 then + if not shiftModifier then modeClick[mode](inputObject) else confirmPlace(inputObject,true) end + end + end) + inputService.TouchStarted:Connect(function(object) + modeClick[mode](object) + end) + inputService.InputEnded:Connect(function(inputObject,irrelevant) + if current and inputObject == pointer then + pointer = nil + current.axis = nil + end + end) + gui.Confirm.Activated:Connect(function() + confirmPlace(nil,true) + end) + _G.PlaceCancelButton.Activated:Connect(function() + if current and current.cancel then current:cancel() end + end) + for _,button in pairs(_G.PlaceModeFrame:GetChildren()) do + if button:IsA("TextButton") then + ModeButtons[button.Name] = button + if mode == button.Name then + button.BackgroundColor3 = Color3.new(0.6, 1, 0.333333) + end + button.Activated:Connect(function() + setMode(button.Name) + modeStart[mode]() + end) + end + end + local moveSnapInput = _G.PlaceSnapInputFrame.MoveSnap + local function evaluateSnapInput(text) + return text:match("([%d%.]+).-") + end + _G.PlaceSnapInputFrame.MoveSnap.FocusLost:Connect(function() + local num = evaluateSnapInput(moveSnapInput.Text) + if num then moveSnap = tonumber(num) end + moveSnapInput.Text = tostring(moveSnap).." stud" + end) + local rotSnapInput = _G.PlaceSnapInputFrame.RotSnap + _G.PlaceSnapInputFrame.RotSnap.FocusLost:Connect(function() + local num = evaluateSnapInput(rotSnapInput.Text) + if num then rotSnap = tonumber(num) end + rotSnapInput.Text = tostring(rotSnap).." deg" + end) + moveSnapInput.Text = tostring(moveSnap).." stud" + rotSnapInput.Text = tostring(rotSnap).." deg" + + local snap_buttons = {} + for _,button in pairs(_G.PlaceModeFrame:GetChildren()) do + if button:IsA("TextButton") then + table.insert(snap_buttons,button) + if mode == button.Name then + button.BackgroundColor3 = Color3.new(0.6, 1, 0.333333) + end + button.Activated:Connect(function() + setMode(button.Name) + modeStart[mode]() + end) + end + end +end + +return getfenv() \ No newline at end of file diff --git a/scripts/modules/ProgramModule.lua b/scripts/modules/ProgramModule.lua new file mode 100644 index 0000000..5a1454f --- /dev/null +++ b/scripts/modules/ProgramModule.lua @@ -0,0 +1,1102 @@ +require(game.ReplicatedFirst.ReadyModule)("ProgramModule") +-- Need interpreter for graph +-- Need program graph for JSON export +-- Need compiler for graph +-- Need data structure links to gui + +--[[ +So I SHOULD definitely work backwards, starting from the interpreter, since it connects to the structure best +The graph is a bit funky... :/ + +So, where does execution even start? Do we have an endpoint and then call everything backwards to bubble up the stack? +Or do we just start from the inputs and try and flow through there? Both sound a bit stupid to be honest. +There are some inputs that just won't really appear. + +There is a concern with loops however, but this probably shouldn't be handled with real physical loops but +more as a processor cycle. I could always increase the clock speed? I'm not sure. +In a way, that means we should iterate through every single object and provide hooks for each one. So lots of coroutines? +Is that sensible? No. Kind of. + +This means every node should have a function to produce a given value associated with it, +an operation to I guess, make that producer? Or does that count. I guess not. +So, the structure should have a list of outputs with types, +a list of inputs with types. +There could also be blocks which have a flag saying that they produce some sort of tangible output. I guess. +So that would be a 'callback'? I guess so. + +So each 'node' in the table structure would just be implicitly in the graph... Uh. Great. Now what. +Well, I guess, how is the JSON data reaaaalllly any different here? +Using IDs doesn't seem like an extraordinarily stupid idea except for adding new tables causes problems. +Ok so we can't use JSON lol. + +]] + +--[[ +Every node would also have its own sort of global rules associated. Which does make me think more about the GUI stuff. +That is indeed quite interesting. +]] +local function class(name) + return {name=name,[name]=true} +end + +-- Language types for everything +local VECTOR = class("Vector") +local STRING = class("String") +local NUMBER = class("Number") +local BOOLEAN = class("Boolean") +local CONTROL = class("Control") -- Enum for vehicle controls +local PIN = class("Pin") +local ANY = class("Any") + +local PINS_LIST = {} +for char in string.gmatch("12345ABCDEXY",".") do table.insert(PINS_LIST,char) end +local CONTROL_LIST = {} +for control in string.gmatch("throttle,yaw,pitch,roll,fire,action1,gear,action2,flaps,bomb,auto","[^,]+") do table.insert(CONTROL_LIST,control) end + +local TYPE_COLOURS = { + Vector = Color3.new(1,0.5,0), + String = Color3.new(0,1,0.5), + Number = Color3.new(0,0.5,1), + Boolean = Color3.new(0.5,0,1), + Control = Color3.new(1,0,0.5), + Pin = Color3.new(0.5,1,0), + Any = Color3.new(0.5,0.5,0.5) +} + +-- Types of row for GUI +local GUI_ROW_TEXT = class("Text") -- Name +local GUI_ROW_INPUT = class("Input") -- Type Name +local GUI_ROW_OUTPUT = class("Output") -- Name Type +local GUI_ROW_LITERAL = class("Literal") -- Name Type Options + +-- Components of the GUI + +local gui + +local programFrame +local programTopBar +local programsList +local programsSelectionBox +local programNameBox +local programUploadButton +local programCancelButton +local programContainer +local programInventory +local programViewport +local programViewportEmpty + +local templateNode +local templateNodeIO +local templateNodeText +local templateNodeSelect +local templateNodeInput +local templateNodeButton +local templateNodeName +local templateProgramTab +local highlight + +if _G.IsClient then + gui = game.Players.LocalPlayer.PlayerGui:WaitForChild("ScreenGui") + + programFrame = _G:WaitFor("ProgramFrame") + programTopBar = programFrame:WaitForChild("TopBar") + programsList = programTopBar:WaitForChild("Programs"):WaitForChild("ScrollingFrame") + programsSelectionBox = programsList.Parent:WaitForChild("Template"):WaitForChild("UIStroke") + programNameBox = programTopBar:WaitForChild("ProgramNameBox") + programUploadButton = programTopBar:WaitForChild("ProgramUploadButton") + programCancelButton = programTopBar:WaitForChild("ProgramCancelButton") + programContainer = programFrame:WaitForChild("Container") + programInventory = programContainer:WaitForChild("Inventory"):WaitForChild("ScrollingFrame") + local programViewportFrame = programContainer:WaitForChild("Program") + programViewport = programViewportFrame:WaitForChild("Viewport") + programViewportEmpty = programViewportFrame:WaitForChild("Empty") + + templateNode = programContainer:WaitForChild("Template"):WaitForChild("Node") + templateNodeIO = templateNode:WaitForChild("IO") + templateNodeText = templateNode:WaitForChild("TextLabel") + templateNodeSelect = templateNode:WaitForChild("LiteralSelect") + templateNodeInput = templateNode:WaitForChild("LiteralInput") + templateNodeButton = templateNode:WaitForChild("LiteralButton") + templateNodeName = templateNode:WaitForChild("NodeLabel") + templateProgramTab = programsList:WaitForChild("Template"):WaitForChild("Script") + highlight = script.Highlight + + templateNode.Visible = false + templateProgramTab.Visible = false +end + +local nodeLibrary = {} + +do -- General nodes library + -- Node constructor functions: + + -- Node input dragged out from the right as a point + local function input(name,type) + return function(node) + table.insert(node.rows,{name = name, text = name, input = type}) + end + end + + -- Node output dragged in from the left as a point + local function output(name,type,producer) + return function(node) + table.insert(node.rows,{name = name, text = name, output = type, producer = producer}) + end + end + + -- Node needs to call a function when done + local function callback(callback) + return function(node) + node.callback = callback + end + end + + -- Node literal value typed in by user + local function literal(name,type,options) + return function(node) + local row = {name = name, text = name, literal = type} + table.insert(node.rows,row) + end + end + + -- Just a row of text on the node + local function text(text) + return function(node) + table.insert(node.rows,{text = text}) + end + end + + -- The name tag at the top of the node + local function name(name) + return function(node) + node.name = name + end + end + + -- The overall node + local function node(...) + local node = { + inputs = {}, + outputs = {}, + rows = {}, + callback = nil, + name = "" + } + for _,item in pairs({...}) do + item(node) + end + return node + end + + -- Raw constructors for nodes in the instruction set + local library = { + node( + name("Control"), + literal("Control",CONTROL), + output("Value",NUMBER,function(node,program) + return program.context.control(node.Control) + end) + ), + node( + name("Input"), + literal("Pin",PIN), + output("Value",ANY,function(node,program) + return program.context.input(node.Pin) + end) + ), + node( + name("Output"), + literal("Pin",PIN), + literal("Header",STRING), + input("Value",ANY), + callback(function(node,program) + program.context.output(node.Pin,node.Header,node.Value()) + end) + ), + node( + name("Print"), + input("Text",ANY), + callback(function(node) + print(node.Text() or "nil") + end) + ), + node( + name("Notify"), + input("Title",ANY), + input("Text",ANY), + callback(function(node,program) + program.context.message(node.Title or "Program Message",node.Text or "Nothing") + end) + ), + node( + name("Number"), + literal("Number",NUMBER), + output("Value",NUMBER,function(node) + return node.Number or 0 + end) + ), + node( + name("Boolean"), + literal("Truth",BOOLEAN), + output("Value",BOOLEAN,function(node) + return node.Truth or false + end) + ), + node( + name("String"), + literal("String",STRING), + output("Value",STRING,function(node) + return node.String or "" + end) + ), + node( + name("Neg"), + input("A",NUMBER), + output("-A",NUMBER,function(node) + return -(tonumber(node.A() or 0) or 0) + end) + ), + node( + name("Con"), + input("A",STRING), + output("A .. B",STRING,function(node) + return (tostring(node.A() or "") or "") .. (tostring(node.B() or "") or "") + end), + input("B",STRING) + ), + node( + name("Add"), + input("A",NUMBER), + output("A + B",NUMBER,function(node) + return (tonumber(node.A() or 0) or 0) + (tonumber(node.B() or 0) or 0) + end), + input("B",NUMBER) + ), + node( + name("Sub"), + input("A",NUMBER), + output("A - B",NUMBER,function(node) + return (tonumber(node.A() or 0) or 0) - (tonumber(node.B() or 0) or 0) + end), + input("B",NUMBER) + ), + node( + name("Div"), + input("A",NUMBER), + output("A / B",NUMBER,function(node) + return (tonumber(node.A() or 1) or 1) / (tonumber(node.B() or 1) or 1) + end), + input("B",NUMBER) + ), + node( + name("Mul"), + input("A",NUMBER), + output("A * B",NUMBER,function(node) + return (tonumber(node.A() or 1) or 1) * (tonumber(node.B() or 1) or 1) + end), + input("B",NUMBER) + ), + node( + name("Comp"), + input("A",NUMBER), + output("A < B",BOOLEAN,function(node) + return (tonumber(node.A() or math.huge) or math.huge) < (tonumber(node.B() or -math.huge) or -math.huge) + end), + input("B",NUMBER) + ), + node( + name("Comp Eq"), + input("A",NUMBER), + output("A <= B",BOOLEAN,function(node) + return (tonumber(node.A() or math.huge) or math.huge) <= (tonumber(node.B() or -math.huge) or -math.huge) + end), + input("B",NUMBER) + ), + node( + name("Cos"), + input("Value",NUMBER), + output("Cosine",NUMBER,function(node) + return math.cos(tonumber(node.Value() or 0) or 0) + end) + ), + node( + name("Sin"), + input("Value",NUMBER), + output("Sin",NUMBER,function(node) + return math.sin(tonumber(node.Value() or 0) or 0) + end) + ), + node( + name("Equal"), + input("A",ANY), + output("A == B",BOOLEAN,function(node) + return node.A() == node.B() + end), + input("B",ANY) + ), + node( + name("Gate"), + input("Continue",BOOLEAN), + output("Output",ANY,function(node) + if node.Continue() then + return node.Input() + else + coroutine.yield() + end + end), + input("Input",ANY) + ), + node( + name("Set"), + literal("Name",STRING), + input("Value",ANY), + callback(function(node,this) + this.variables[node.Name()] = node.Value + end) + ), + node( + name("Get"), + literal("Name",STRING), + output("Value",ANY,function(node,this) + return this.variables[node.Name()] + end) + ), + node( + name("Time"), + output("Value",ANY,function(node,this) + return tick() + end) + ), + node( + name("Frequency"), + literal("Number",NUMBER,{min = 1, max = 60}), + callback(function(node,program) + program.frequency = node.Number or 30 + end) + ) + } + + -- Create all node frames for the library to select nodes from + local function init() + programFrame.Visible = false + for index,node in ipairs(library) do + local frame = templateNode:Clone() + -- Remove all things already in the template, they may not be needed. + for _,item in pairs(frame:GetChildren()) do + if item:IsA("GuiObject") then item:Destroy() end + end + -- Node name tag at the top + local name = templateNodeName:Clone() + name.Text = node.name or error("Node missing a name") + name.Parent = frame + -- Massive if thingy to make all the different types of row in a node + for index,row in ipairs(node.rows) do + local item + if row.input or row.output then -- Input/Output + local io = templateNodeIO:Clone() + io.Point.BackgroundColor3 = TYPE_COLOURS[(row.input or row.output).name] + if row.input then + io.Label.TextXAlignment = Enum.TextXAlignment.Left + elseif row.output then + io.Point.Position = UDim2.new(1,6,0.5,0) + io.Label.TextXAlignment = Enum.TextXAlignment.Right + end + io.Label.Text = row.text + item = io + elseif row.literal then -- Literal + local literal + local type = row.literal + if row.literal == NUMBER or row.literal == STRING then + literal = templateNodeInput:Clone() + literal.Text = "" + if row.literal == NUMBER then + literal.PlaceholderText = "0" + elseif row.literal == STRING then + literal.PlaceholderText = "Text" + end + elseif row.literal == BOOLEAN then + literal = templateNodeButton:Clone() + literal.Text = "False" + elseif row.literal == PIN or row.literal == CONTROL then + literal = templateNodeSelect:Clone() + if row.literal == PIN then + literal.Value.Text = "1" + elseif row.literal == CONTROL then + literal.Value.Text = "throttle" + end + else + error("No row type for "..tostring(row.literal)) + end + item = literal + elseif row.text then -- Text + local text = templateNodeText:Clone() + text.Text = row.text + item = text + else + error("Row doesn't satisfy any template rows") + end + item.Parent = frame + item.LayoutOrder = index + if row.name then item.Name = row.name end + end + -- Darker nodes indicate where the rest of the nodes get originally called from! + if node.callback then frame.UIStroke.Enabled = true end + + frame.LayoutOrder = index + frame.Parent = programInventory + frame.Visible = true + + node.frame = frame + library[frame] = node + end + end + + if _G.IsClient then init() end + + for index,node in pairs(library) do + library[node.name] = node + end + + nodeLibrary = library +end + +-- Neat way of making classes in Lua +local function factory() + local meta = {} + meta.__index = meta + setmetatable(meta,{__call = function(meta,...) + return setmetatable(meta.new(...),meta) + end}) + return meta +end + +local currentProgram +local editSource +local openProgram + +-- The infamous dragger +local dragger = function(a,b) return require(_G.Modules.DraggingModule).dragger(a,b,programFrame) end + +do -- General nodes programming + local programs = {} -- Live program data being edited. programs[program{options...=...,node{}...}] + + -- Make a line shape to be going from the two AbsolutePositions given + local function position_line(line,start,finish) + local diff = start-finish + local rotation = math.atan2(diff.y,diff.x) + local mid = finish:Lerp(start,0.5) + line.Position = UDim2.new(0,mid.X-line.Parent.AbsolutePosition.X,0,mid.Y-line.Parent.AbsolutePosition.Y) + line.Rotation = math.deg(rotation) + line.Size = UDim2.new(0,(diff.Magnitude)+line.Size.Y.Offset,0,line.Size.Y.Offset) + end + + local row = factory() + row.new = function(template,node) + local this = {} + this.parent = node + this.parent.rows[template.name] = this + this.template = template + this.frame = nil + if this.template.input then + this.link = nil + this.line = nil + end + if this.template.output then + this.links = {} + end + return this + end + local function matches(this,other) + local function types(input,output) + if not (input.template.input and output.template.output) then return nil end + if input.template.input == output.template.output then return input.template.input,input,output + elseif input.template.input == ANY then return output.template.output,input,output + elseif output.template.output == ANY then return input.template.input,input,output + else return ANY,input,output end + end + local typed,i,o = types(this,other) + if typed then return typed,i,o end + local typed,i,o = types(other,this) + if typed then return typed,i,o end + return nil + end + row.adopt = function(this,frame) + this.frame = frame + if this.template.output or this.template.input then + local point = frame.Point + this.parent.parent.points[point] = this + dragger(point,function(initial) + -- Create a line from the point we are dragging from + local line = frame.Point:Clone() + line.AnchorPoint = Vector2.new(0.5,0.5) + line.Parent = gui + local color = line.BackgroundColor3 + -- Offset of point, something to do with AnchorPoint I think... + local offset = Vector2.new(frame.Point.Size.X.Offset,frame.Point.Size.Y.Offset)/2 + -- Other point connected to + local connected + return { + drag = function(position) + local position = Vector2.new(position.X,position.Y) + local start = frame.Point.AbsolutePosition + offset + position_line(line,start,position) + line.BackgroundColor3 = color + connected = nil + for _,object in _G.Gui:GetGuiObjectsAtPosition(position.X,position.Y) do + local row = this.parent.parent.points[object] + if row then + local typed = matches(this,row) + if typed then + local point = row.frame.Point + connected = row + position_line(line,start,point.AbsolutePosition + offset) + line.BackgroundColor3 = TYPE_COLOURS[typed.name] + end + end + end + end, + ended = function() + if connected then + local typed,input,output = matches(this,connected) + input:connect(output) + end + line:Destroy() + end, + } + end) + elseif this.template.literal then + local typed = this.template.literal + if typed == NUMBER or typed == STRING then + if typed == NUMBER then this.value = this.value or 0 + elseif typed == STRING then this.value = this.value or "" + end + this.frame.Text = tostring(this.value) + this.frame.FocusLost:Connect(function(enter) + if enter then + if typed == NUMBER then + this.value = tonumber(this.frame.Text) or 0 + elseif typed == STRING then + this.value = string.sub(this.frame.Text or "",1,25565) + end + this.frame.Text = tostring(this.value) + end + end) + elseif typed == BOOLEAN then + if this.value then this.frame.Text = "True" else this.frame.Text = "False" end + this.frame.Activated:Connect(function() + this.value = not this.value + if this.value then this.frame.Text = "True" else this.frame.Text = "False" end + end) + elseif typed == PIN or typed == CONTROL then + local tab + if typed == PIN then + tab = PINS_LIST + elseif typed == CONTROL then + tab = CONTROL_LIST + end + this.value = this.value or tab[1] + this.frame.Value.Text = this.value + this.frame.Next.Activated:Connect(function() + local index = table.find(tab,this.value) + 1 + if index > #tab then index = 1 end + this.value = tab[index] + this.frame.Value.Text = this.value + end) + this.frame.Previous.Activated:Connect(function() + local index = table.find(tab,this.value) - 1 + if index < 1 then index = #tab end + this.value = tab[index] + this.frame.Value.Text = this.value + end) + else + error("No row type for "..tostring(row.literal)) + end + end + end + -- Make a row's line position correctly if it exists + row.reconnect = function(this) + if this.template.input and this.link and this.line then + local offset = Vector2.new(this.frame.Point.Size.X.Offset,this.frame.Point.Size.Y.Offset)/2 + position_line( + this.line, + this.frame.Point.AbsolutePosition + offset, + this.link.frame.Point.AbsolutePosition + offset + ) + elseif this.template.output then + -- As an output we have multiple links, inputs have one, let's just let the inputs do it. + for _,input in pairs(this.links) do + input:reconnect() + end + end + end + -- Disconnect a node from it's incoming link + row.disconnect = function(this) + if this.template.output then + -- Just pass it to the input to handle this. + local links = {} + for _,row in pairs(this.links) do + table.insert(links,row) + end + for _,row in pairs(links) do + row:disconnect() + end + --error("Can't disconnect from output, do it on input.",2) + elseif this.template.input then + if this.link then -- Check if it is linked + --print("disc",debug.traceback()) + -- Remove that link from whatever it was linked to as well + table.remove(this.link.links,table.find(this.link.links,this)) + -- Remove it here + this.link = nil + -- Remove the GUI + if this.line then this.line:Destroy() this.line = nil end + end + end + end + row.close = function(this) + if this.template.output or this.template.input then + if this.line then this.line:Destroy() this.line = nil end + this.parent.parent.points[this.frame.Point] = nil + this.frame = nil + -- No need for this.frame:Destroy() since a row can only be destroyed when a node is, and a row is in the node's frame + end + end + row.connect = function(this,other) + if this.template.output then + -- It's not really good to do this either. Input's only have one, simple. + error("Can't connect from output, do it on input.",2) + elseif this.template.input then + this:disconnect() + this.link = other -- Link up here + table.insert(other.links,this) -- Link up on the output too + --print(this,debug.traceback()) + if this.frame then -- Create a line if we have a frame + local line = this.frame.Point:Clone() + line.AnchorPoint = Vector2.new(0.5,0.5) + line.Parent = programViewport + line.ZIndex = line.ZIndex - 1 + line.InputEnded:Connect(function(input) + if input.UserInputType == Enum.UserInputType.MouseButton1 or + input.UserInputType == Enum.UserInputType.Touch then + this:disconnect() + end + end) + line.BackgroundColor3 = TYPE_COLOURS[matches(this,other).name] + this.line = line + end + this:reconnect() + end + end + + local node = factory() + node.new = function(template,parent) + local this = {} + this.template = template + -- Row: {frame, template, } + -- Node: {...,rows[int] = {},[producer_name] = function() end...,[literal_name] = value...} + this.rows = {} + for _,template in ipairs(template.rows) do + row(template,this) + end + this.parent = parent + this.position = Vector2.new(0,0) + this.frame = nil + return this + end + -- Adopt a frame that this node will respect + node.adopt = function(this,frame) + this.position = frame.AbsolutePosition - programViewport.AbsolutePosition + this.frame = frame + dragger(frame.NodeLabel,function(initial) + local offset = frame.AbsolutePosition - initial - programViewport.AbsolutePosition + return { + drag = function(position) + frame.Position = UDim2.new(0,position.X + offset.X,0,position.Y + offset.Y) + this.position = frame.AbsolutePosition - programViewport.AbsolutePosition + for _,row in pairs(this.rows) do + row:reconnect() + end + end, + ended = function(position) + -- TODO: Add the stupid out of bounds check + end, + } + end) + frame.NodeLabel.CloseButton.Activated:Connect(function() + this:destroy() + this:close() + end) + for _,row in pairs(this.rows) do + -- For every I/O row in the node + row:adopt(this.frame:FindFirstChild(row.template.name)) + end + end + -- Remove all associated GUI + node.close = function(this) + if this.frame then + this.position = this.frame.AbsolutePosition - programViewport.AbsolutePosition + for _,row in pairs(this.rows) do row:close() end + this.frame:Destroy() + this.frame = nil + end + end + -- Remove all links to other structures + node.destroy = function(this) + for _,row in pairs(this.rows) do + row:disconnect() + end + table.remove(this.parent.nodes,table.find(this.parent.nodes,this)) + end + -- Program data that can be executed and has GUI instances bound to it + local program = factory() + -- Literally write 'program()' to do program.new + program.new = function(value,upload) + local this = {} + if upload then + if programs[value] then + return programs[value] + else + programs[value] = this -- Add it to the programs list end + end + end + this.upload_callback = upload + this.value = value + this.name = "Unnamed" + this.frequency = 30 + this.nodes = {} + this.points = {} -- point to row + this.tab = nil + this.variables = {} + -- Hooks for the value changing in game, the whole program needs to be updated in this case. + if value.Changed then + value.Changed:Connect(function() + if currentProgram == this then this:close() end + this:load() + this:compile() + if currentProgram == this then this:open() end + print("reloaded") + -- TODO: Add a program.new here so that users don't lose their program data! + end) + end + if value.Destroying then + value.Destroying:Connect(function() + this:close() -- ... So why are they still there then? + if this.tab then this.tab:Destroy() end + programs[this.value] = nil + end) + end + return this + end + -- Make program adopt a tab + program.adopt = function(this,tab) + this.tab = tab + this.tab.Text = this.name + this.tab.Close.Activated:Connect(function() + wait() + this:close() + this.tab:Destroy() + this.tab = nil + end) + this.tab.Activated:Connect(function() + this:open() + end) + end + -- Load into live program data from JSON + NAME_INDEX = 1 -- For each row and each node too! + POSITION_INDEX = 2 + ROWS_INDEX = 3 + VALUE_INDEX = 2 + LINK_ID_INDEX = 2 + LINK_ROW_INDEX = 3 + INDEX_TO_NAME = {} + NAME_TO_INDEX = {} + for word in ("Value,Input,Output,Text,Add,Div,Mul,Neg,Boolean,Number,Print,Message,Output,Input"):gmatch("[^,%s]+") do -- This string CANNOT change. + table.insert(INDEX_TO_NAME,word) + end + for index,item in pairs(INDEX_TO_NAME) do + NAME_TO_INDEX[item] = index + end + --print(NAME_TRANSLATION) + program.load = function(this) + local current = currentProgram == this + xpcall(function() -- Protection on saves + local text = this.value.Value + + if not text then return end + if text == "" then return end + + local data = _G.Http:JSONDecode(text) + + this.name = data.name + this.version = data.version + if data.position then this.position = Vector2.new(data.position[1],data.position[2]) end + + this.nodes = {} + for index,node in ipairs(data.nodes) do + local name = node[NAME_INDEX] + local template = nodeLibrary[INDEX_TO_NAME[name] or name] + if not template then warn("Could not find node of name: "..INDEX_TO_NAME[name] or name) print(nodeLibrary) continue end + this.nodes[index] = this:node(template) + this.nodes[index].position = Vector2.new(node[POSITION_INDEX][1],node[POSITION_INDEX][2]) + end + + for index,data_node in ipairs(data.nodes) do + local node = this.nodes[index] + for _,data_row in pairs(data_node[ROWS_INDEX]) do + local name = data_row[NAME_INDEX] + local row = node.rows[INDEX_TO_NAME[name] or name] + if not row then warn("Could not find row of name: "..(INDEX_TO_NAME[name] or name)) continue end + if row.template.input then + row:connect(this.nodes[data_row[LINK_ID_INDEX]].rows[INDEX_TO_NAME[data_row[LINK_ROW_INDEX]] or data_row[LINK_ROW_INDEX]]) + elseif row.template.literal then + row.value = data_row[VALUE_INDEX] + end + end + end + + if this.tab then this.tab.Text = this.name end + this:compile() + end,function(err) + warn(debug.traceback(err,2)) + end) + end + -- Serialises the live program data + program.save = function(this) + local data = {nodes = {}, name = this.name, version = (this.version or 0) + 1} + + if this.position then data.position = {this.position.X,this.position.Y} end + + local ids = {} + -- Make a table where we can get the id of a node using it as a key, ordered. + for index,node in ipairs(this.nodes) do + ids[node] = index + if node.frame then node.position = node.frame.AbsolutePosition - programViewport.AbsolutePosition end + end + + for _,node in ipairs(this.nodes) do + local rows = {} + for _,row in pairs(node.rows) do + local name = row.template.name + if row.template.input and row.link then + table.insert(rows,{NAME_TO_INDEX[name] or name,ids[row.link.parent],NAME_TO_INDEX[row.link.template.name] or row.link.template.name}) + elseif row.template.literal then + table.insert(rows,{NAME_TO_INDEX[name] or name,row.value}) + end + end + data.nodes[ids[node]] = { + [NAME_INDEX] = NAME_TO_INDEX[node.template.name] or node.template.name, + [POSITION_INDEX] = {node.position.X,node.position.Y}, + [ROWS_INDEX] = rows + } + end + + return _G.Http:JSONEncode(data) + end + program.upload = function(this) + this.upload_callback(this:save()) + end + -- Connects everything together in a program so it is accessible from the node + program.compile = function(this) + for _,node in pairs(this.nodes) do + for _,row in pairs(node.rows) do + if row.template.input then + if row.link then + node[row.template.name] = function() return row.link.template.producer(row.link.parent,this) end + else + node[row.template.name] = function() end + end + end + if row.template.literal then + node[row.template.name] = row.value + end + end + end + end + -- Run a program from its data + program.run = function(this,context) + program.context = context + for _,node in pairs(this.nodes) do + if node.template.callback then coroutine.wrap(_G.Protect(node.template.callback))(node,this) end + end + end + -- Open a program into the editor/viewport + program.open = function(this) + -- TODO: Add a selection box around the currently operating tab + -- TODO: Add a background tint if the script has not been saved yet + if currentProgram then currentProgram:close() end + + currentProgram = this + programViewportEmpty.Visible = false + highlight.Adornee = this.value.Parent + programsSelectionBox.Parent = this.tab + for _,node in ipairs(this.nodes) do + local frame = node.template.frame:Clone() + frame.Parent = programViewport + frame.Position = UDim2.new(0,node.position.X,0,node.position.Y) + node:adopt(frame) + end + if this.position then + local pos = programViewport.Position + programViewport.Position = UDim2.new(pos.X.Scale,this.position.X,pos.Y.Scale,this.position.Y) + end + -- This is quite bad: + for _,node in ipairs(this.nodes) do + for _,row in pairs(node.rows) do + if row.link then row:connect(row.link) end -- To get links to regenerate after everything is adopted + end + end + end + -- Remove all GUI hooks in data + program.close = function(this) + for _,node in pairs(this.nodes) do + node:close() + end + if _G.IsClient then + programViewportEmpty.Visible = true + programsSelectionBox.Parent = nil + currentProgram = nil + highlight.Adornee = nil + end + end + -- Remove a node, resolve connections and remove GUI + program.remove = function(this,node) + node:close() + node:destroy() + table.remove(this.nodes,table.find(this.nodes,node)) + end + -- Add a node, candidly on the ScreenGui (lol) + program.node = function(this,...) + local node = node(...,this) + table.insert(this.nodes,node) + return node + end + -- Make a new program, add it to the list, read into it and open it out. + local function edit(value,upload) + -- Value is a StringValue instance or a table binding to this, so return if it's already here! + + local this = program(value,upload) + + if not this.tab then + local tab = templateProgramTab:Clone() + tab.Parent = programsList + tab.Visible = true + this:adopt(tab) + end + + if currentProgram then currentProgram:close() end + this:load() + this:open() + end + + editSource = edit + openProgram = program +end + +if _G.IsClient then -- General GUI interaction + programCancelButton.Activated:Connect(function() + programFrame.Visible = false + if currentProgram then currentProgram:close() end + end) + programUploadButton.Activated:Connect(function() + if currentProgram then + local program = currentProgram + _G.Events.Notification:Fire("Program Upload","Attempting program upload.") + program:upload() + -- Optional, this should always work but it's not necessary + program:close() + program:load() + program:open() + else + _G.Events.Notification:Fire("Program Upload","No program is currently loaded.") + end + end) + dragger(programViewport,function(last) + local lower = Vector2.new(-math.huge,-math.huge) + local upper = Vector2.new(math.huge,math.huge) + if currentProgram then + for _,node in pairs(currentProgram.nodes) do + node = node.frame + lower = Vector2.max(lower,node.AbsolutePosition - programViewport.AbsolutePosition) + upper = Vector2.min(upper,node.AbsolutePosition - programViewport.AbsolutePosition + node.AbsoluteSize - programViewport.AbsoluteSize) + end + end + local original = programViewport.Position + local positions = {} + return { + drag = function(position) + local offset = position - last + programViewport.Position = UDim2.new(original.X.Scale,original.X.Offset + offset.X,original.Y.Scale,original.Y.Offset + offset.Y) + currentProgram.position = Vector2.new(original.X.Offset + offset.X,original.Y.Offset + offset.Y) + --[[if not currentProgram then return end + for _,node in pairs(currentProgram.nodes) do + if not positions[node] then positions[node] = node.position end + node.position = positions[node] + Vector2.min(Vector2.max(position - last,-lower),-upper) + node.frame.Position = UDim2.new(0,node.position.X,0,node.position.Y) + for _,row in pairs(node.rows) do + row:reconnect() + end + end]] + end + } + end) + --[[local pinchState + local pinchScale + programViewport.TouchPinch:Connect(function(_,scale,_,state) + pinchState = state + pinchScale + end) + ]] + programViewport.MouseWheelBackward:Connect(function() + programViewport.UIScale.Scale = math.clamp(programViewport.UIScale.Scale - 0.1,0.2,2) + end) + programViewport.MouseWheelForward:Connect(function() + programViewport.UIScale.Scale = math.clamp(programViewport.UIScale.Scale + 0.1,0.2,2) + end) + programNameBox.FocusLost:Connect(function(enter) + if enter and currentProgram then + currentProgram.name = string.sub(programNameBox.Text,1,128) + currentProgram.tab.Text = currentProgram.name + end + end) + -- GIVEN 'init()' on inventory + for _,frame in pairs(programInventory:GetChildren()) do + if nodeLibrary[frame] then -- This is a real node frame we can place + frame.ZIndex = -1 + dragger(frame.NodeLabel,function(initial) + local offset = frame.AbsolutePosition - initial + frame.Parent = gui + return { + drag = function(position) + frame.Position = UDim2.new(0,position.X + offset.X,0,position.Y + offset.Y) + end, + ended = function(position) + local lowX = programViewport.AbsolutePosition.X + local highX = lowX + programViewport.AbsoluteSize.X + local lowY = programViewport.AbsolutePosition.Y + local highY = lowY + programViewport.AbsoluteSize.Y + if position.X > lowX and + position.X < highX and + position.Y > lowY and + position.Y < highY and + currentProgram + then + -- Create a new node frame + local template = frame + local frame = template:Clone() + local position = template.AbsolutePosition - programViewport.AbsolutePosition + template.Parent = programInventory + frame.Parent = programViewport + frame.Position = UDim2.new(0,position.X,0,position.Y) + -- Create new node data + local node = currentProgram:node(nodeLibrary[template]) + node:adopt(frame) -- Make the data adopt the frame + end + frame.Parent = programInventory + end, + } + end) + end + end +end + +return {edit_source = editSource, open_program = openProgram} \ No newline at end of file diff --git a/scripts/modules/RainModule.lua b/scripts/modules/RainModule.lua new file mode 100644 index 0000000..5ee404d --- /dev/null +++ b/scripts/modules/RainModule.lua @@ -0,0 +1,1059 @@ +-- I DEFINATITELY DIDNT STAEL THIS I SWARE +--[[ + + Rain module v1.0 by buildthomas (July 2018) + + This module is licensed under the APLv2: + http://www.apache.org/licenses/LICENSE-2.0 + + In short, you may use this code only if you agree to the following: + * This notice must always be present and may not be modified or removed in any copy of this code or derived code. + * You may use this in commercial, closed source projects, and you may modify the source code itself. + + Refer to the license for a full description. + + For questions please reach out on the Developer Forum (@buildthomas) + or via Twitter (https://www.twitter.com/buildthomasRBX) + + ------ + + + Rain:Enable( tweenInfo) + Enable the rain effects instantly, or over a given easing function if tweenInfo is given. + + Rain:Disable( tweenInfo) + Disable the rain effects instantly, or over a given easing function if tweenInfo is given. + + + Rain:SetColor( color, tweenInfo) + Set the global color of all rain particles to a given Color3 value. + Sets the color instantly, or over a given easing function if tweenInfo is given. + Color sequences are not supported because this would lead to a messy effect. + The starting value is RAIN_DEFAULT_COLOR. + + Rain:SetTransparency( transparency, tweenInfo) + Set the global transparency of all rain effects. 0 = regular visibility, 1 = fully invisible. + Sets the transparency instantly, or over a given easing function if tweenInfo is given. + Clamped between 0 and 1, the starting value is RAIN_DEFAULT_TRANSPARENCY. + + Rain:SetSpeedRatio( ratio, tweenInfo) + Set the vertical falling speed of the rain particles. 0 = still, 1 = max falling speed. + Sets the speed instantly, or over a given easing function if tweenInfo is given. + Clamped between 0 and 1, the starting value is RAIN_DEFAULT_SPEEDRATIO. + + Rain:SetIntensityRatio( ratio, tweenInfo) + Set the intensity of the rain. 0 = no effects, 1 = full effects. + Sets the intensity instantly, or over a given easing function if tweenInfo is given. + Clamped between 0 and 1, the starting value is RAIN_DEFAULT_INTENSITYRATIO. + + Rain:SetLightEmission( ratio, tweenInfo) + Set the global light emission of all rain effects. + Sets the light emission instantly, or over a given easing function if tweenInfo is given. + Clamped between 0 and 1, the starting value is RAIN_DEFAULT_LIGHTEMISSION. + + Rain:SetLightInfluence( transparency, tweenInfo) + Set the global light influence of all rain effects. + Sets the light influence instantly, or over a given easing function if tweenInfo is given. + Clamped between 0 and 1, the starting value is RAIN_DEFAULT_LIGHTINFLUENCE. + + + Rain:SetVolume( volume, tweenInfo) + Set the global max volume of rain instantly, or over a given easing function if tweenInfo is given. + The initial volume of the rain's soundgroup is RAIN_SOUND_BASEVOLUME. + + + Rain:SetCeiling(> ceiling) + Set a Y coordinate that marks the ceiling of the world. Above this spot, rain will act as if it's indoors. + Feed nil to remove any previously set ceiling. + + + Rain:SetDirection( direction, tweenInfo) + Set the direction that rain falls from. The direction parameter should be a unit direction. + Sets the rain direction instantly, or over a given easing function if tweenInfo is given. + + Rain:SetStraightTexture( asset) + Rain:SetTopDownTexture( asset) + Rain:SetSplashTexture( asset) + Adjust textures of the rain effect. + + Rain:SetSoundId( asset) + Adjust sound effect of the rain effect. + + Rain:SetCollisionMode( mode, ...) + Sets the current way collisions are determined for the rain. + + Rain.CollisionMode + A table that should be seen as an enumerator for the collision mode. + The following values are available: + * Rain.CollisionMode.None - All parts in the default collision group will block the rain. + * Rain.CollisionMode.Whitelist - Use the whitelist provided by Rain::SetCollisionWhitelist. + * Rain.CollisionMode.Blacklist - Use the blacklist provided by Rain::SetCollisionBlacklist. + * Rain.CollisionMode.Function - Use the test function provided by Rain::SetCollisionFunction and do deep-casts. + + Rain:SetCollisionMode(Rain.CollisionMode.None) + No parameters. + + Rain:SetCollisionMode(Rain.CollisionMode.Whitelist, > whitelist) + The provided value can either be a hierarchy of objects or a table of objects to filter with. + + Rain:SetCollisionMode(Rain.CollisionMode.Blacklist, > blacklist) + The provided value can either be a hierarchy of objects or a table of objects to filter out. + + Rain:SetCollisionMode(Rain.CollisionMode.Function, boolean>> f) + If f(part) returns a value that lua evaluates to a true condition, that part can be hit by rain. + If f(part) returns any other value, that part cannot be hit by the rain. + + +--]] + +-- Options: + +local MIN_SIZE = Vector3.new(0.05,0.05,0.05) -- Size of main emitter part when rain inactive + +local RAIN_DEFAULT_COLOR = Color3.new(1,1,1) -- Default color3 of all rain elements +local RAIN_DEFAULT_TRANSPARENCY = 0 -- Default transparency scale ratio of all rain elements +local RAIN_DEFAULT_SPEEDRATIO = 1 -- Default speed scale ratio of falling rain effects +local RAIN_DEFAULT_INTENSITYRATIO = 1 -- Default intensity ratio of all rain elements +local RAIN_DEFAULT_LIGHTEMISSION = 0.05 -- Default LightEmission of all rain elements +local RAIN_DEFAULT_LIGHTINFLUENCE = 0.9 -- Default LightInfluence of all rain elements +local RAIN_DEFAULT_DIRECTION = Vector3.new(0,-1,0) -- Default direction for rain to fall into + +local RAIN_TRANSPARENCY_T1 = .25 -- Define the shape (time-wise) of the transparency curves for emitters +local RAIN_TRANSPARENCY_T2 = .75 + +local RAIN_SCANHEIGHT = 1000 -- How many studs to scan up from camera position to determine whether occluded + +local RAIN_EMITTER_DIM_DEFAULT = 40 -- Size of emitter block to the side/up +local RAIN_EMITTER_DIM_MAXFORWARD = 100 -- Size of emitter block forwards when looking at the horizon +local RAIN_EMITTER_UP_MODIFIER = 20 -- Maximum vertical displacement of emitter (when looking fully up/down) + +local RAIN_SOUND_ASSET = "rbxassetid://1516791621" +local RAIN_SOUND_BASEVOLUME = 0.2 -- Starting volume of rain sound effect when not occluded +local RAIN_SOUND_FADEIN_TIME = 1 -- Tween in/out times for sound volume +local RAIN_SOUND_FADEOUT_TIME = 1 + +local RAIN_STRAIGHT_ASSET = "rbxassetid://1822883048" -- Some properties of the straight rain particle effect +local RAIN_STRAIGHT_ALPHA_LOW = 0.7 -- Minimum particle transparency for the straight rain emitter +local RAIN_STRAIGHT_SIZE = NumberSequence.new(10) +local RAIN_STRAIGHT_LIFETIME = NumberRange.new(0.8) +local RAIN_STRAIGHT_MAX_RATE = 600 -- Maximum rate for the straight rain emitter +local RAIN_STRAIGHT_MAX_SPEED = 60 -- Maximum speed for the straight rain emitter + +local RAIN_TOPDOWN_ASSET = "rbxassetid://1822856633" -- Some properties of the top-down rain particle effect +local RAIN_TOPDOWN_ALPHA_LOW = 0.85 -- Minimum particle transparency for the top-down rain emitter +local RAIN_TOPDOWN_SIZE = NumberSequence.new { + NumberSequenceKeypoint.new(0, 5.33, 2.75); + NumberSequenceKeypoint.new(1, 5.33, 2.75); +} +local RAIN_TOPDOWN_LIFETIME = NumberRange.new(0.8) +local RAIN_TOPDOWN_ROTATION = NumberRange.new(0,360) +local RAIN_TOPDOWN_MAX_RATE = 600 -- Maximum rate for the top-down rain emitter +local RAIN_TOPDOWN_MAX_SPEED = 60 -- Maximum speed for the top-down rain emitter + +local RAIN_SPLASH_ASSET = "rbxassetid://1822856633" -- Some properties of the splash particle effect +local RAIN_SPLASH_ALPHA_LOW = 0.6 -- Minimum particle transparency for the splash emitters +local RAIN_SPLASH_SIZE = NumberSequence.new { + NumberSequenceKeypoint.new(0, 0); + NumberSequenceKeypoint.new(.4, 3); + NumberSequenceKeypoint.new(1, 0); +} +local RAIN_SPLASH_LIFETIME = NumberRange.new(0.1, 0.15) +local RAIN_SPLASH_ROTATION = NumberRange.new(0,360) +local RAIN_SPLASH_NUM = 20 -- Amount of splashes per frame +local RAIN_SPLASH_CORRECTION_Y = .5 -- Offset from impact position for visual reasons +local RAIN_SPLASH_STRAIGHT_OFFSET_Y = 50 -- Offset against rain direction for straight rain particles from splash position +local RAIN_NOSPLASH_STRAIGHT_OFFSET_Y_MIN = 20 -- Min/max vertical offset from camera height for straight rain particles +local RAIN_NOSPLASH_STRAIGHT_OFFSET_Y_MAX = 100 -- when no splash position could be found (i.e. no floor at that XZ-column) + +local RAIN_OCCLUDED_MINSPEED = 70 -- Minimum speed for the occluded straight rain emitters +local RAIN_OCCLUDED_MAXSPEED = 100 -- Maximum speed for the occluded straight rain emitters +local RAIN_OCCLUDED_SPREAD = Vector2.new(10,10) -- Spread angle for the occluded straight rain emitters +local RAIN_OCCLUDED_MAXINTENSITY = 2 -- How many occluded straight rain particles are emitted for every splash for max intensity + +local RAIN_OCCLUDECHECK_OFFSET_Y = 500 -- Vertical offset from camera height to start scanning downward from for splashes +local RAIN_OCCLUDECHECK_OFFSET_XZ_MIN = -100 -- Range of possible XZ offset values from camera XZ position for the splashes +local RAIN_OCCLUDECHECK_OFFSET_XZ_MAX = 100 +local RAIN_OCCLUDECHECK_SCAN_Y = 550 -- Scan magnitude along rain path + +local RAIN_UPDATE_PERIOD = 6 -- Update the transparency of the main emitters + volume of rain inside every X frames + +local RAIN_VOLUME_SCAN_RADIUS = 35 -- Defining grid for checking how far the camera is away from a spot exposed to rain +local RAIN_VOLUME_SCAN_GRID = { -- Unit range grid for scanning how far away user is from rain space + -- range 0.2, 4 pts + Vector3.new(0.141421363, 0, 0.141421363); + Vector3.new(-0.141421363, 0, 0.141421363); + Vector3.new(-0.141421363, 0, -0.141421363); + Vector3.new(0.141421363, 0, -0.141421363); + -- range 0.4, 8 pts + Vector3.new(0.400000006, 0, 0); + Vector3.new(0.282842726, 0, 0.282842726); + Vector3.new(2.44929371e-17, 0, 0.400000006); + Vector3.new(-0.282842726, 0, 0.282842726); + Vector3.new(-0.400000006, 0, 4.89858741e-17); + Vector3.new(-0.282842726, 0, -0.282842726); + Vector3.new(-7.34788045e-17, 0, -0.400000006); + Vector3.new(0.282842726, 0, -0.282842726); + -- range 0.6, 10 pts + Vector3.new(0.600000024, 0, 0); + Vector3.new(0.485410213, 0, 0.352671146); + Vector3.new(0.185410202, 0, 0.570633948); + Vector3.new(-0.185410202, 0, 0.570633948); + Vector3.new(-0.485410213, 0, 0.352671146); + Vector3.new(-0.600000024, 0, 7.34788112e-17); + Vector3.new(-0.485410213, 0, -0.352671146); + Vector3.new(-0.185410202, 0, -0.570633948); + Vector3.new(0.185410202, 0, -0.570633948); + Vector3.new(0.485410213, 0, -0.352671146); + -- range 0.8, 12 pts + Vector3.new(0.772740662, 0, 0.207055241); + Vector3.new(0.565685451, 0, 0.565685451); + Vector3.new(0.207055241, 0, 0.772740662); + Vector3.new(-0.207055241, 0, 0.772740662); + Vector3.new(-0.565685451, 0, 0.565685451); + Vector3.new(-0.772740662, 0, 0.207055241); + Vector3.new(-0.772740662, 0, -0.207055241); + Vector3.new(-0.565685451, 0, -0.565685451); + Vector3.new(-0.207055241, 0, -0.772740662); + Vector3.new(0.207055241, 0, -0.772740662); + Vector3.new(0.565685451, 0, -0.565685451); + Vector3.new(0.772740662, 0, -0.207055241); +} + + +-- Enumerators: + +local CollisionMode = { + None = 0; + Whitelist = 1; + Blacklist = 2; + Function = 3; +} + + +-- Variables & setup: + +-- services +local Players = game:GetService("Players") +local TweenService = _G.Tween +local RunService = game:GetService("RunService") + +local GlobalModifier = Instance.new("NumberValue") -- modifier for rain visibility for disabling/enabling over time span +GlobalModifier.Value = 1 -- 0 = fully visible, 1 = invisible + +local connections = {} -- Stores connections to RunService signals when enabled + +local disabled = true -- Value to figure out whether we are moving towards a disabled state (useful during tweens) + +local rainDirection = RAIN_DEFAULT_DIRECTION -- Direction that rain falls into + +local currentCeiling = nil -- Y coordinate of ceiling (if present) + +local collisionMode = CollisionMode.None -- Collision mode (from Rain.CollisionMode) for raycasting +local collisionList = nil -- Blacklist/whitelist for raycasting +local collisionFunc = nil -- Raycasting test function for when collisionMode == Rain.CollisionMode.Function + +local straightLowAlpha = 1 -- Current transparency for straight rain particles +local topdownLowAlpha = 1 -- Current transparency for top-down rain particles +local intensityOccludedRain = 0 -- Current intensity of occluded rain particles +local numSplashes = 0 -- Current number of generated splashes per frame +local volumeTarget = 0 -- Current (target of tween for) sound volume + +-- shorthands +local v3 = Vector3.new +local NSK010 = NumberSequenceKeypoint.new(0, 1, 0) +local NSK110 = NumberSequenceKeypoint.new(1, 1, 0) + +local volumeScanGrid = {} -- Pre-generate grid used for raining area distance scanning +for _,v in pairs(RAIN_VOLUME_SCAN_GRID) do + table.insert(volumeScanGrid, v * RAIN_VOLUME_SCAN_RADIUS) +end +table.sort(volumeScanGrid, function(a,b) -- Optimization: sort from close to far away for fast evaluation if closeby + return a.magnitude < b.magnitude +end) + +-- sound group for easy main volume tweaking +local SoundGroup = Instance.new("SoundGroup") +SoundGroup.Name = "__RainSoundGroup" +SoundGroup.Volume = RAIN_SOUND_BASEVOLUME +SoundGroup.Archivable = false + +local Sound = Instance.new("Sound") +Sound.Name = "RainSound" +Sound.Volume = volumeTarget +Sound.SoundId = RAIN_SOUND_ASSET +Sound.Looped = true +Sound.SoundGroup = SoundGroup +Sound.Parent = SoundGroup +Sound.Archivable = false + +-- emitter block around camera used when outside +local Emitter do + + Emitter = Instance.new("Part") + Emitter.Transparency = 1 + Emitter.Anchored = true + Emitter.CanCollide = false + Emitter.Locked = false + Emitter.Archivable = false + Emitter.TopSurface = Enum.SurfaceType.Smooth + Emitter.BottomSurface = Enum.SurfaceType.Smooth + Emitter.Name = "__RainEmitter" + Emitter.Size = MIN_SIZE + Emitter.Archivable = false + + local straight = Instance.new("ParticleEmitter") + straight.Name = "RainStraight" + straight.LightEmission = RAIN_DEFAULT_LIGHTEMISSION + straight.LightInfluence = RAIN_DEFAULT_LIGHTINFLUENCE + straight.Size = RAIN_STRAIGHT_SIZE + straight.Texture = RAIN_STRAIGHT_ASSET + straight.LockedToPart = true + straight.Enabled = false + straight.Lifetime = RAIN_STRAIGHT_LIFETIME + straight.Rate = RAIN_STRAIGHT_MAX_RATE + straight.Speed = NumberRange.new(RAIN_STRAIGHT_MAX_SPEED) + straight.EmissionDirection = Enum.NormalId.Bottom + straight.Parent = Emitter + straight.Orientation = Enum.ParticleOrientation.FacingCameraWorldUp + + local topdown = Instance.new("ParticleEmitter") + topdown.Name = "RainTopDown" + topdown.LightEmission = RAIN_DEFAULT_LIGHTEMISSION + topdown.LightInfluence = RAIN_DEFAULT_LIGHTINFLUENCE + topdown.Size = RAIN_TOPDOWN_SIZE + topdown.Texture = RAIN_TOPDOWN_ASSET + topdown.LockedToPart = true + topdown.Enabled = false + topdown.Rotation = RAIN_TOPDOWN_ROTATION + topdown.Lifetime = RAIN_TOPDOWN_LIFETIME + topdown.Rate = RAIN_TOPDOWN_MAX_RATE + topdown.Speed = NumberRange.new(RAIN_TOPDOWN_MAX_SPEED) + topdown.EmissionDirection = Enum.NormalId.Bottom + topdown.Parent = Emitter + +end + +local splashAttachments, rainAttachments do + + splashAttachments = {} + rainAttachments = {} + + for i = 1, RAIN_SPLASH_NUM do + + -- splashes on ground + local splashAttachment = Instance.new("Attachment") + splashAttachment.Name = "__RainSplashAttachment" + local splash = Instance.new("ParticleEmitter") + splash.LightEmission = RAIN_DEFAULT_LIGHTEMISSION + splash.LightInfluence = RAIN_DEFAULT_LIGHTINFLUENCE + splash.Size = RAIN_SPLASH_SIZE + splash.Texture = RAIN_SPLASH_ASSET + splash.Rotation = RAIN_SPLASH_ROTATION + splash.Lifetime = RAIN_SPLASH_LIFETIME + splash.Transparency = NumberSequence.new { + NSK010; + NumberSequenceKeypoint.new(RAIN_TRANSPARENCY_T1, RAIN_SPLASH_ALPHA_LOW, 0); + NumberSequenceKeypoint.new(RAIN_TRANSPARENCY_T2, RAIN_SPLASH_ALPHA_LOW, 0); + NSK110; + } + splash.Enabled = false + splash.Rate = 0 + splash.Speed = NumberRange.new(0) + splash.Name = "RainSplash" + splash.Parent = splashAttachment + splashAttachment.Archivable = false + table.insert(splashAttachments, splashAttachment) + + -- occluded rain particle generation + local rainAttachment = Instance.new("Attachment") + rainAttachment.Name = "__RainOccludedAttachment" + local straightOccluded = Emitter.RainStraight:Clone() + straightOccluded.Speed = NumberRange.new(RAIN_OCCLUDED_MINSPEED, RAIN_OCCLUDED_MAXSPEED) + straightOccluded.SpreadAngle = RAIN_OCCLUDED_SPREAD + straightOccluded.LockedToPart = false + straightOccluded.Enabled = false + straightOccluded.Parent = rainAttachment + local topdownOccluded = Emitter.RainTopDown:Clone() + topdownOccluded.Speed = NumberRange.new(RAIN_OCCLUDED_MINSPEED, RAIN_OCCLUDED_MAXSPEED) + topdownOccluded.SpreadAngle = RAIN_OCCLUDED_SPREAD + topdownOccluded.LockedToPart = false + topdownOccluded.Enabled = false + topdownOccluded.Parent = rainAttachment + rainAttachment.Archivable = false + table.insert(rainAttachments, rainAttachment) + + end + +end + + +-- Helper methods: + +local ignoreEmitterList = { Emitter } + +local raycastFunctions = { + [CollisionMode.None] = function(ray, ignoreCharacter) + return workspace:FindPartOnRayWithIgnoreList(ray, ignoreCharacter and {Emitter, Players.LocalPlayer and Players.LocalPlayer.Character} or ignoreEmitterList) + end; + [CollisionMode.Blacklist] = function(ray) + return workspace:FindPartOnRayWithIgnoreList(ray, collisionList) + end; + [CollisionMode.Whitelist] = function(ray) + return workspace:FindPartOnRayWithWhitelist(ray, collisionList) + end; + [CollisionMode.Function] = function(ray) + local destination = ray.Origin + ray.Direction + -- draw multiple raycasts concatenated to each other until no hit / valid hit found + while ray.Direction.magnitude > 0.001 do + local part, pos, norm, mat = workspace:FindPartOnRayWithIgnoreList(ray, ignoreEmitterList) + if not part or collisionFunc(part) then + return part, pos, norm, mat + end + local start = pos + ray.Direction.Unit * 0.001 + ray = Ray.new(start, destination - start) + end + end; +} +local raycast = raycastFunctions[collisionMode] + +local function connectLoop() + + local rand = Random.new() + + local inside = true -- Whether camera is currently in a spot occluded from the sky + local frame = RAIN_UPDATE_PERIOD -- Frame counter, and force update cycle right now + + -- Update Emitter on RenderStepped since it needs to be synced to Camera + table.insert(connections, RunService.RenderStepped:connect(function() + + -- Check if camera is outside or inside + local part, position = raycast(Ray.new(workspace.CurrentCamera.CFrame.p, -rainDirection * RAIN_SCANHEIGHT), true) + + if (not currentCeiling or workspace.CurrentCamera.CFrame.p.y <= currentCeiling) and not part then + + -- Camera is outside and under ceiling + + if volumeTarget < 1 and not disabled then + volumeTarget = 1 + TweenService:Create(Sound, TweenInfo.new(.5), {Volume = 1}):Play() + end + + frame = RAIN_UPDATE_PERIOD + + local t = math.abs(workspace.CurrentCamera.CFrame.lookVector:Dot(rainDirection)) + + local center = workspace.CurrentCamera.CFrame.p + local right = workspace.CurrentCamera.CFrame.lookVector:Cross(-rainDirection) + right = right.magnitude > 0.001 and right.unit or -rainDirection + local forward = rainDirection:Cross(right).unit + + Emitter.Size = v3( + RAIN_EMITTER_DIM_DEFAULT, + RAIN_EMITTER_DIM_DEFAULT, + RAIN_EMITTER_DIM_DEFAULT + (1 - t)*(RAIN_EMITTER_DIM_MAXFORWARD - RAIN_EMITTER_DIM_DEFAULT) + ) + + Emitter.CFrame = + CFrame.new( + center.x, center.y, center.z, + right.x, -rainDirection.x, forward.x, + right.y, -rainDirection.y, forward.y, + right.z, -rainDirection.z, forward.z + ) + + (1 - t) * workspace.CurrentCamera.CFrame.lookVector * Emitter.Size.Z/3 + - t * rainDirection * RAIN_EMITTER_UP_MODIFIER + + Emitter.RainStraight.Enabled = true + Emitter.RainTopDown.Enabled = true + + inside = false + + else + + -- Camera is inside / above ceiling + + Emitter.RainStraight.Enabled = false + Emitter.RainTopDown.Enabled = false + + inside = true + + end + + end)) + + -- Do the other effects on Stepped + local signal = RunService:IsRunning() and RunService.Stepped or RunService.RenderStepped + table.insert(connections, signal:connect(function() + + frame = frame + 1 + + -- Only do some updates once every few frames + if frame >= RAIN_UPDATE_PERIOD then + + -- Measure of how much camera is facing down (0-1) + local t = math.abs(workspace.CurrentCamera.CFrame.lookVector:Dot(rainDirection)) + + -- More looking down = see straight particles less and see top-down particles more + local straightSequence = NumberSequence.new { + NSK010; + NumberSequenceKeypoint.new(RAIN_TRANSPARENCY_T1, (1 - t)*straightLowAlpha + t, 0); + NumberSequenceKeypoint.new(RAIN_TRANSPARENCY_T2, (1 - t)*straightLowAlpha + t, 0); + NSK110; + } + local topdownSequence = NumberSequence.new { + NSK010; + NumberSequenceKeypoint.new(RAIN_TRANSPARENCY_T1, t*topdownLowAlpha + (1 - t), 0); + NumberSequenceKeypoint.new(RAIN_TRANSPARENCY_T2, t*topdownLowAlpha + (1 - t), 0); + NSK110; + } + + -- Find desired rotation for the straight rain particles + local mapped = workspace.Camera.CFrame:inverse() * (workspace.Camera.CFrame.p - rainDirection) + local straightRotation = NumberRange.new(math.deg(math.atan2(-mapped.x, mapped.y))) + + if inside then + + -- Update emitter properties + for _,v in pairs(rainAttachments) do + v.RainStraight.Transparency = straightSequence + v.RainStraight.Rotation = straightRotation + v.RainTopDown.Transparency = topdownSequence + end + + if not disabled then + + -- Only do occluded volume check if not moving towards disabled state + + local volume = 0 + + if (not currentCeiling or workspace.CurrentCamera.CFrame.p.y <= currentCeiling) then + + -- Check how far away camera is from a space open to the sky using volume scan grid + + local minDistance = RAIN_VOLUME_SCAN_RADIUS + local rayDirection = -rainDirection * RAIN_SCANHEIGHT + + for i = 1, #volumeScanGrid do -- In order, so first hit is closest + if not raycast(Ray.new(workspace.CurrentCamera.CFrame * volumeScanGrid[i], rayDirection), true) then + minDistance = volumeScanGrid[i].magnitude + break + end + end + + -- Volume is inversely proportionate to minimum distance + volume = 1 - minDistance / RAIN_VOLUME_SCAN_RADIUS + + end + + if math.abs(volume - volumeTarget) > .01 then + -- Value is sufficiently different from previous target, overwrite it + volumeTarget = volume + TweenService:Create(Sound, TweenInfo.new(1), {Volume = volumeTarget}):Play() + end + + end + + else + + -- Update emitter properties + Emitter.RainStraight.Transparency = straightSequence + Emitter.RainStraight.Rotation = straightRotation + Emitter.RainTopDown.Transparency = topdownSequence + + end + + -- Reset frame counter + frame = 0 + + end + + local center = workspace.CurrentCamera.CFrame.p + local right = workspace.CurrentCamera.CFrame.lookVector:Cross(-rainDirection) + right = right.magnitude > 0.001 and right.unit or -rainDirection + local forward = rainDirection:Cross(right).unit + local transform = CFrame.new( + center.x, center.y, center.z, + right.x, -rainDirection.x, forward.x, + right.y, -rainDirection.y, forward.y, + right.z, -rainDirection.z, forward.z + ) + local rayDirection = rainDirection * RAIN_OCCLUDECHECK_SCAN_Y + + -- Splash and occlusion effects + for i = 1, numSplashes do + + local splashAttachment = splashAttachments[i] + local rainAttachment = rainAttachments[i] + + -- Sample random splash position + local x = rand:NextNumber(RAIN_OCCLUDECHECK_OFFSET_XZ_MIN, RAIN_OCCLUDECHECK_OFFSET_XZ_MAX) + local z = rand:NextNumber(RAIN_OCCLUDECHECK_OFFSET_XZ_MIN, RAIN_OCCLUDECHECK_OFFSET_XZ_MAX) + local part, position, normal = raycast(Ray.new(transform * v3(x, RAIN_OCCLUDECHECK_OFFSET_Y, z), rayDirection)) + + if part then + + -- Draw a splash at hit + splashAttachment.Position = position + normal * RAIN_SPLASH_CORRECTION_Y + splashAttachment.RainSplash:Emit(1) + + if inside then + + -- Draw occlusion rain particles a little bit above the splash position + local corrected = position - rainDirection * RAIN_SPLASH_STRAIGHT_OFFSET_Y + if currentCeiling and corrected.Y > currentCeiling and rainDirection.Y < 0 then + corrected = corrected + rainDirection * (currentCeiling - corrected.Y) / rainDirection.Y + end + rainAttachment.CFrame = transform - transform.p + corrected + rainAttachment.RainStraight:Emit(intensityOccludedRain) + rainAttachment.RainTopDown:Emit(intensityOccludedRain) + + end + + elseif inside then + + -- Draw occlusion rain particles on the XZ-position at around the camera's height + local corrected = transform * v3(x, rand:NextNumber(RAIN_NOSPLASH_STRAIGHT_OFFSET_Y_MIN, RAIN_NOSPLASH_STRAIGHT_OFFSET_Y_MAX), z) + if currentCeiling and corrected.Y > currentCeiling and rainDirection.Y < 0 then + corrected = corrected + rainDirection * (currentCeiling - corrected.Y) / rainDirection.Y + end + rainAttachment.CFrame = transform - transform.p + corrected + rainAttachment.RainStraight:Emit(intensityOccludedRain) + rainAttachment.RainTopDown:Emit(intensityOccludedRain) + + end + + end + + end)) + +end + +local function disconnectLoop() + -- If present, disconnect all RunService connections + if #connections > 0 then + for _,v in pairs(connections) do + v:disconnect() + end + connections = {} + end +end + +local function disableSound(tweenInfo) + + -- Tween the rain sound to be mute over a given easing function + volumeTarget = 0 + local tween = TweenService:Create(Sound, tweenInfo, {Volume = 0}) + tween.Completed:connect(function(state) + if state == Enum.PlaybackState.Completed then + Sound:Stop() + end + tween:Destroy() + end) + tween:Play() + +end + +local function disable() + + disconnectLoop() + + -- Hide Emitter + Emitter.RainStraight.Enabled = false + Emitter.RainTopDown.Enabled = false + Emitter.Size = MIN_SIZE + + -- Disable sound now if not tweened into disabled state beforehand + if not disabled then + disableSound(TweenInfo.new(RAIN_SOUND_FADEOUT_TIME)) + end + +end + +-- Shorthand for creating a tweenable "variable" using value object +local function makeProperty(valueObjectClass, defaultValue, setter) + local valueObject = Instance.new(valueObjectClass) + if defaultValue then + valueObject.Value = defaultValue + end + valueObject.Changed:connect(setter) + setter(valueObject.Value) + return valueObject +end + +local Color = makeProperty("Color3Value", RAIN_DEFAULT_COLOR, function(value) + + local value = ColorSequence.new(value) + + Emitter.RainStraight.Color = value + Emitter.RainTopDown.Color = value + + for _,v in pairs(splashAttachments) do + v.RainSplash.Color = value + end + for _,v in pairs(rainAttachments) do + v.RainStraight.Color = value + v.RainTopDown.Color = value + end + +end) + +local function updateTransparency(value) + + local opacity = (1 - value) * (1 - GlobalModifier.Value) + local transparency = 1 - opacity + + straightLowAlpha = RAIN_STRAIGHT_ALPHA_LOW * opacity + transparency + topdownLowAlpha = RAIN_TOPDOWN_ALPHA_LOW * opacity + transparency + + local splashSequence = NumberSequence.new { + NSK010; + NumberSequenceKeypoint.new(RAIN_TRANSPARENCY_T1, opacity*RAIN_SPLASH_ALPHA_LOW + transparency, 0); + NumberSequenceKeypoint.new(RAIN_TRANSPARENCY_T2, opacity*RAIN_SPLASH_ALPHA_LOW + transparency, 0); + NSK110; + } + + for _,v in pairs(splashAttachments) do + v.RainSplash.Transparency = splashSequence + end + +end +local Transparency = makeProperty("NumberValue", RAIN_DEFAULT_TRANSPARENCY, updateTransparency) +GlobalModifier.Changed:connect(updateTransparency) + +local SpeedRatio = makeProperty("NumberValue", RAIN_DEFAULT_SPEEDRATIO, function(value) + + Emitter.RainStraight.Speed = NumberRange.new(value * RAIN_STRAIGHT_MAX_SPEED) + Emitter.RainTopDown.Speed = NumberRange.new(value * RAIN_TOPDOWN_MAX_SPEED) + +end) + +local IntensityRatio = makeProperty("NumberValue", RAIN_DEFAULT_INTENSITYRATIO, function(value) + + Emitter.RainStraight.Rate = RAIN_STRAIGHT_MAX_RATE * value + Emitter.RainTopDown.Rate = RAIN_TOPDOWN_MAX_RATE * value + + intensityOccludedRain = math.ceil(RAIN_OCCLUDED_MAXINTENSITY * value) + numSplashes = RAIN_SPLASH_NUM * value + +end) + +local LightEmission = makeProperty("NumberValue", RAIN_DEFAULT_LIGHTEMISSION, function(value) + + Emitter.RainStraight.LightEmission = value + Emitter.RainTopDown.LightEmission = value + + for _,v in pairs(rainAttachments) do + v.RainStraight.LightEmission = value + v.RainTopDown.LightEmission = value + end + for _,v in pairs(splashAttachments) do + v.RainSplash.LightEmission = value + end + +end) + +local LightInfluence = makeProperty("NumberValue", RAIN_DEFAULT_LIGHTINFLUENCE, function(value) + + Emitter.RainStraight.LightInfluence = value + Emitter.RainTopDown.LightInfluence = value + + for _,v in pairs(rainAttachments) do + v.RainStraight.LightInfluence = value + v.RainTopDown.LightInfluence = value + end + for _,v in pairs(splashAttachments) do + v.RainSplash.LightInfluence = value + end + +end) + +local RainDirection = makeProperty("Vector3Value", RAIN_DEFAULT_DIRECTION, function(value) + if value.magnitude > 0.001 then + rainDirection = value.unit + end +end) + + +-- Exposed API: + +local Rain = {} + +Rain.CollisionMode = CollisionMode + +function Rain:Enable(tweenInfo) + + if tweenInfo ~= nil and typeof(tweenInfo) ~= "TweenInfo" then + error("bad argument #1 to 'Enable' (TweenInfo expected, got " .. typeof(tweenInfo) .. ")", 2) + end + + disconnectLoop() -- Just in case :Enable(..) is called multiple times on accident + + Emitter.RainStraight.Enabled = true + Emitter.RainTopDown.Enabled = true + Emitter.Parent = workspace.CurrentCamera + + for i = 1, RAIN_SPLASH_NUM do + splashAttachments[i].Parent = workspace.Terrain + rainAttachments[i].Parent = workspace.Terrain + end + + if RunService:IsRunning() then -- don't need sound in studio preview, it won't work anyway + SoundGroup.Parent = game:GetService("SoundService") + end + + connectLoop() + + if tweenInfo then + TweenService:Create(GlobalModifier, tweenInfo, {Value = 0}):Play() + else + GlobalModifier.Value = 0 + end + + if not Sound.Playing then + Sound:Play() + Sound.TimePosition = math.random()*Sound.TimeLength + end + + disabled = false + +end + +function Rain:Disable(tweenInfo) + + if tweenInfo ~= nil and typeof(tweenInfo) ~= "TweenInfo" then + error("bad argument #1 to 'Disable' (TweenInfo expected, got " .. typeof(tweenInfo) .. ")", 2) + end + + if tweenInfo then + local tween = TweenService:Create(GlobalModifier, tweenInfo, {Value = 1}) + tween.Completed:connect(function(state) + if state == Enum.PlaybackState.Completed then + -- Only disable the rain completely once the visual effects are faded out + disable() + end + tween:Destroy() + end) + tween:Play() + -- Start tweening out sound now as well + disableSound(tweenInfo) + else + GlobalModifier.Value = 1 + disable() + end + + disabled = true + +end + +function Rain:SetColor(value, tweenInfo) + + if typeof(value) ~= "Color3" then + error("bad argument #1 to 'SetColor' (Color3 expected, got " .. typeof(value) .. ")", 2) + elseif tweenInfo ~= nil and typeof(tweenInfo) ~= "TweenInfo" then + error("bad argument #2 to 'SetColor' (TweenInfo expected, got " .. typeof(tweenInfo) .. ")", 2) + end + + if tweenInfo then + TweenService:Create(Color, tweenInfo, {Value = value}):Play() + else + Color.Value = value + end + +end + +local function makeRatioSetter(methodName, valueObject) + -- Shorthand because most of the remaining property setters are very similar + return function(_, value, tweenInfo) + + if typeof(value) ~= "number" then + error("bad argument #1 to '" .. methodName .. "' (number expected, got " .. typeof(value) .. ")", 2) + elseif tweenInfo ~= nil and typeof(tweenInfo) ~= "TweenInfo" then + error("bad argument #2 to '" .. methodName .. "' (TweenInfo expected, got " .. typeof(tweenInfo) .. ")", 2) + end + + value = math.clamp(value, 0, 1) + + if tweenInfo then + TweenService:Create(valueObject, tweenInfo, {Value = value}):Play() + else + valueObject.Value = value + end + + end +end + +Rain.SetTransparency = makeRatioSetter("SetTransparency", Transparency) +Rain.SetSpeedRatio = makeRatioSetter("SetSpeedRatio", SpeedRatio) +Rain.SetIntensityRatio = makeRatioSetter("SetIntensityRatio", IntensityRatio) +Rain.SetLightEmission = makeRatioSetter("SetLightEmission", LightEmission) +Rain.SetLightInfluence = makeRatioSetter("SetLightInfluence", LightInfluence) + +function Rain:SetVolume(volume, tweenInfo) + + if typeof(volume) ~= "number" then + error("bad argument #1 to 'SetVolume' (number expected, got " .. typeof(volume) .. ")", 2) + elseif tweenInfo ~= nil and typeof(tweenInfo) ~= "TweenInfo" then + error("bad argument #2 to 'SetVolume' (TweenInfo expected, got " .. typeof(tweenInfo) .. ")", 2) + end + + if tweenInfo then + TweenService:Create(SoundGroup, tweenInfo, {Volume = volume}):Play() + else + SoundGroup.Volume = volume + end + +end + +function Rain:SetDirection(direction, tweenInfo) + + if typeof(direction) ~= "Vector3" then + error("bad argument #1 to 'SetDirection' (Vector3 expected, got " .. typeof(direction) .. ")", 2) + elseif tweenInfo ~= nil and typeof(tweenInfo) ~= "TweenInfo" then + error("bad argument #2 to 'SetDirection' (TweenInfo expected, got " .. typeof(tweenInfo) .. ")", 2) + end + + if not (direction.unit.magnitude > 0) then -- intentional statement formatting since NaN comparison + warn("Attempt to set rain direction to a zero-length vector, falling back on default direction = (" .. tostring(RAIN_DEFAULT_DIRECTION) .. ")") + direction = RAIN_DEFAULT_DIRECTION + end + + if tweenInfo then + TweenService:Create(RainDirection, tweenInfo, {Value = direction}):Play() + else + RainDirection.Value = direction + end + +end + +function Rain:SetCeiling(ceiling) + + if ceiling ~= nil and typeof(ceiling) ~= "number" then + error("bad argument #1 to 'SetCeiling' (number expected, got " .. typeof(ceiling) .. ")", 2) + end + + currentCeiling = ceiling + +end + +function Rain:SetStraightTexture(asset) + + if typeof(asset) ~= "string" then + error("bad argument #1 to 'SetStraightTexture' (string expected, got " .. typeof(asset) .. ")", 2) + end + + Emitter.RainStraight.Texture = asset + + for _,v in pairs(rainAttachments) do + v.RainStraight.Texture = asset + end + +end + +function Rain:SetTopDownTexture(asset) + + if typeof(asset) ~= "string" then + error("bad argument #1 to 'SetStraightTexture' (string expected, got " .. typeof(asset) .. ")", 2) + end + + Emitter.RainTopDown.Texture = asset + + for _,v in pairs(rainAttachments) do + v.RainTopDown.Texture = asset + end + +end + +function Rain:SetSplashTexture(asset) + + if typeof(asset) ~= "string" then + error("bad argument #1 to 'SetStraightTexture' (string expected, got " .. typeof(asset) .. ")", 2) + end + + for _,v in pairs(splashAttachments) do + v.RainSplash.Texture = asset + end + +end + +function Rain:SetSoundId(asset) + + if typeof(asset) ~= "string" then + error("bad argument #1 to 'SetSoundId' (string expected, got " .. typeof(asset) .. ")", 2) + end + + Sound.SoundId = asset + +end + +function Rain:SetCollisionMode(mode, param) + + if mode == CollisionMode.None then + + -- Regular mode needs no white/blacklist or test function + collisionList = nil + collisionFunc = nil + + elseif mode == CollisionMode.Blacklist then + + if typeof(param) == "Instance" then + -- Add Emitter anyway, since users will probably not expect collisions with emitter block regardless + collisionList = {param, Emitter} + elseif typeof(param) == "table" then + for i = 1, #param do + if typeof(param[i]) ~= "Instance" then + error("bad argument #2 to 'SetCollisionMode' (blacklist contained a " .. typeof(param[i]) .. " on index " .. tostring(i) .. " which is not an Instance)", 2) + end + end + collisionList = {Emitter} -- see above + for i = 1, #param do + table.insert(collisionList, param[i]) + end + else + error("bad argument #2 to 'SetCollisionMode (Instance or array of Instance expected, got " .. typeof(param) .. ")'", 2) + end + + -- Blacklist does not need a test function + collisionFunc = nil + + elseif mode == CollisionMode.Whitelist then + + if typeof(param) == "Instance" then + collisionList = {param} + elseif typeof(param) == "table" then + for i = 1, #param do + if typeof(param[i]) ~= "Instance" then + error("bad argument #2 to 'SetCollisionMode' (whitelist contained a " .. typeof(param[i]) .. " on index " .. tostring(i) .. " which is not an Instance)", 2) + end + end + collisionList = {} + for i = 1, #param do + table.insert(collisionList, param[i]) + end + else + error("bad argument #2 to 'SetCollisionMode (Instance or array of Instance expected, got " .. typeof(param) .. ")'", 2) + end + + -- Whitelist does not need a test function + collisionFunc = nil + + elseif mode == CollisionMode.Function then + + if typeof(param) ~= "function" then + error("bad argument #2 to 'SetCollisionMode' (function expected, got " .. typeof(param) .. ")", 2) + end + + -- Test function does not need a list + collisionList = nil + + collisionFunc = param + + else + error("bad argument #1 to 'SetCollisionMode (Rain.CollisionMode expected, got " .. typeof(param) .. ")'", 2) + end + + collisionMode = mode + raycast = raycastFunctions[mode] + +end + +return Rain \ No newline at end of file diff --git a/scripts/modules/RuleModule.lua b/scripts/modules/RuleModule.lua new file mode 100644 index 0000000..a342757 --- /dev/null +++ b/scripts/modules/RuleModule.lua @@ -0,0 +1,27 @@ +local module = {} + +local rules = {} +module.rules = rules +local listeners = {} + +function module.listenRule(name,callback) + if rules[name] then task.spawn(callback,rules[name]) end + if not listeners[name] then listeners[name] = {} end + table.insert(listeners[name],callback) +end + +function module.setRule(name,value) + _G.Remotes.Rule:FireAllClients(name,value) + if not rules[name] then rules[name] = {} end + if not listeners[name] then listeners[name] = {} end + rules[name] = value + for _,callback in pairs(listeners[name]) do + task.spawn(callback,value) + end +end + +if _G.IsClient then + _G.Remotes.Rule.OnClientEvent:Connect(module.setRule) +end + +return module \ No newline at end of file diff --git a/scripts/modules/SaveModule.lua b/scripts/modules/SaveModule.lua new file mode 100644 index 0000000..97c4d9b --- /dev/null +++ b/scripts/modules/SaveModule.lua @@ -0,0 +1,503 @@ +-- 41458171 + +require(game.ReplicatedFirst.ReadyModule)("SaveModule") +local Commands = require(_G.Modules.CommandsModule) +Placement = require(_G.Modules.PlacementModule) +Debris = _G.Debris + +SaveFrame = nil +Stroke = nil +SaveData = nil +SaveTabs = nil +SaveLists = nil +Templates = nil +PlayerSaveList = nil +SelectionBox = nil + +parts = _G.Storage:WaitForChild("Parts") + +SubassemblyData = "" +SubassemblyModel = nil + +CurrentSaveData = "" +CurrentPlacementConfig = nil + +CurrentSaveRoot = nil +LastInteractedPart = nil + +SelectionBoxes = {} + +local function OffloadSelection() + if CurrentPlacementConfig then CurrentPlacementConfig:stop() end + CurrentSaveRoot = nil + CurrentSaveData = nil + SelectionBox.Adornee = nil + for _,object in pairs(SelectionBoxes) do + object:Destroy() + end + SelectionBoxes = {} +end + +local function LoadModel(key,group) + wait() + local raw = _G.Remotes.Load:InvokeServer(group,key) + local success,result = pcall(function() + return _G.Methods.saves.dataToModel(raw,false) + end) + local model + if success then + model = result + else + _G.Error(string.format("Load Model Error (%s:%s): %s",key,group,result)) + end + if not model then + return _G.Storage.Missing:Clone() + end + local primary = model:FindFirstChildWhichIsA("VehicleSeat",true) + if primary then + model.PrimaryPart = primary + end + return model +end + +local _tab = {} +_tab.__index = _tab + +local _slot = {} +_slot.__index = _slot +_slot.__newindex_operations = { + name = function(this,name) + this.frame.NameBox.Text = name + end, + order = function(this,index) + this.frame.LayoutOrder = tonumber(index) or 0 + this.frame.IdLabel.Text = tostring(index):upper() + if index == "autosave" then + this.frame.IdLabel.BackgroundColor3 = Color3.new(1, 0.768627, 0.223529) + elseif index == "subassembly" then + this.frame.IdLabel.BackgroundColor3 = Color3.new(0.623529, 0.0941176, 1) + end + end, + on_mirror_clicked = function(this,callback) + _G.Error("Mirror function unimplemented") + end, + on_name_change = function(this,callback) + this.frame.NameBox.TextEditable = true + this.frame.NameBox.FocusLost:Connect(function(enter) + if enter then callback(this.frame.NameBox.Text) end + end) + end, + on_save_clicked = function(this,callback) + this.frame.SaveButton.Visible = true + this.frame.SaveButton.Activated:Connect(function() + callback(CurrentSaveData) + end) + end, + on_load_clicked = function(this,callback) + this.frame.LoadButton.Activated:Connect(function() + callback(this) + end) + end, + model = function(this,model) + local old = this.viewport:FindFirstChildWhichIsA("Model") + if old then old:Destroy() end + if not model then + this.frame.OldLabel.Visible = true + this.frame.OldLabel.Text = "CORRUPT" + return + else + if not (model.Name == "Model") then this.frame.NameBox.Text = model.Name end + end + model.Parent = this.viewport + this.frame.OldLabel.Visible = model:HasTag("Old") + this.frame.OldLabel.Text = model:GetAttribute("Version") or "Err" + local cframe,size = model:GetBoundingBox() + this.camera.CFrame = cframe * CFrame.fromEulerAnglesYXZ(-0.5,-2.5,0) + this.camera.CFrame = this.camera.CFrame + this.camera.CFrame.LookVector * -(size.Magnitude/2) + end +} +_slot.__newindex = function(this,index,value) + if not _slot.__newindex_operations[index] then error("No index to set: "..index,2) end + _slot.__newindex_operations[index](this,value) +end + +_slot.show = function(this) + this.frame.Visible = true + _G.Tween:Create(this.frame.UIPadding,TweenInfo.new(0.5,Enum.EasingStyle.Quad,Enum.EasingDirection.Out),{PaddingLeft = UDim.new(0,0)}):Play() +end + +_tab.CreateSlot = function(this) + local new = {} + new.frame = Templates.Frame:Clone() + new.frame.Parent = this.list + new.raw = "" + _G.Union(new.frame,{ + Parent = this.list, + Visible = true, + }) + new.frame.NameBox.TextEditable = false + new.frame.SaveButton.Visible = false + + new.angle = 45 + + new.viewport = new.frame.ViewportFrame + new.undo = new.frame.UndoButton + new.refresh = new.frame.RefreshButton + + new.camera = Instance.new("Camera") + new.camera.Parent = new.viewport + new.camera.CFrame = CFrame.Angles(0,math.rad(new.angle),0) + new.viewport.CurrentCamera = new.camera + + local hover = false + new.viewport.MouseEnter:Connect(function() + hover = true + new.viewport.BackgroundColor3 = Color3.new(0.75,0.75,0.75) + while wait() and hover do + new.camera.CFrame = CFrame.Angles(0,math.rad(2),0) * new.camera.CFrame + end + end) + + new.viewport.MouseLeave:Connect(function() + hover = false + new.viewport.BackgroundColor3 = Color3.new(1,1,1) + end) + + new.frame.Visible = false + new.frame.OldLabel.Visible = false + + return setmetatable(new,_slot) +end + +_tab.FocusSection = function(this) + for _,list in pairs(SaveLists:GetChildren()) do + if list:IsA("ScrollingFrame") then list.Visible = false end + end + this.list.Visible = true + Stroke.Parent = this.tab +end + +local function CreateSection(title) + local new = setmetatable({},_tab) + new.list = Templates.List:Clone() + _G.Union(new.list,{ + Parent = SaveLists, + Visible = false + }) + new.tab = Templates.Tab:Clone() + _G.Union(new.tab,{ + Parent = SaveTabs, + Visible = true, + Text = title + }) + new.slots = {} + new.tab.CloseButton.Activated:Connect(function() + Stroke.Parent = PlayerSaveList + new.tab:Destroy() + new.list:Destroy() + PlayerSaveList:FocusSection() + end) + new.tab.Activated:Connect(function() new:FocusSection() end) + return new +end + +_G.SaveIndices = {} +local function CreateUserSection(userid,modify,close) + userid = tostring(userid or _G.UserId) + local title = (game.Players:GetPlayerByUserId(tonumber(userid) or 0) or {DisplayName = userid}).DisplayName + local section = CreateSection(title) + if userid == tostring(_G.UserId) then section:FocusSection() end + local save_info = {} + local save_info_locked = false + local saves = {} + local slots = {} + if not userid:match(":") and not close then section.tab.CloseButton.Visible = false end + local function update_save_info(callback) + repeat + wait() + until not save_info_locked + save_info_locked = true + if callback then callback(save_info) end + _G.Remotes.SetSaveInfo:InvokeServer(save_info,userid) + save_info = _G.Remotes.GetSaveInfo:InvokeServer(userid) + for index,name in pairs(save_info) do + local slot = slots[tostring(index)] or slots[index] + if slot then + slot.name = name + end + end + save_info_locked = false + end + spawn(function() + save_info = _G.Remotes.GetSaveInfo:InvokeServer(userid) + local indices = {} + if not save_info then return end + for index,_ in pairs(save_info) do + table.insert(indices,index) + end + _G.SaveIndices[userid] = indices + if not userid:match(":") then + table.sort(indices,function(before,after) + before = tonumber(before) + after = tonumber(after) + if not before then return true end + if not after then return false end + return before < after + end) + end + for _,index in pairs(indices) do + spawn(function() + local name = save_info[index] + index = tostring(index) + local slot = section:CreateSlot() + slots[index] = slot + local model = LoadModel(index,userid) + saves[index] = model + _G.Union(slot,{ + name = name, + order = index, + on_name_change = function(text) + update_save_info(function() + save_info[tonumber(index) or index] = text + end) + end, + on_load_clicked = function() + if not saves[index] or not saves[index].PrimaryPart then _G.Events.Notification:Fire("Vehicle Load Failed","There is no vehicle.") return end + OffloadSelection() + CurrentPlacementConfig = Placement.begin(saves[index]:Clone(),{ + confirm = function(config) + _G.Remotes.Place:InvokeServer(config.model.PrimaryPart.CFrame,index,userid) + config:stop() + end, + stopped = function(config) + config.model:Destroy() + CurrentPlacementConfig = nil + end, + disabled = { + resize = true + } + }) + end, + model = model + }) + if modify then + slot.on_save_clicked = function() + if (not CurrentSaveData) or (#CurrentSaveData == 0) then + _G.Events.Notification:Fire("Vehicle Save Failed","Please select a vehicle to save") + return + end + _G.Remotes.Save:InvokeServer(CurrentSaveData,index,userid) + model = LoadModel(index,userid) + saves[index] = model + slot.model = model + OffloadSelection() + end + slot.viewport.MouseEnter:Connect(function() + slot.undo.Visible = true + slot.refresh.Visible = true + end) + slot.viewport.MouseLeave:Connect(function() + slot.undo.Visible = false + slot.refresh.Visible = false + end) + end + slot.undo.Activated:Connect(function() + CreateUserSection(userid..":"..index,false) + end) + slot.refresh.Activated:Connect(function() + model = LoadModel(index,userid) + saves[index] = model + slot.model = model + OffloadSelection() + end) + slot:show() + end) + wait(0.2) + end + + if tostring(_G.UserId) == userid then -- autosave + spawn(function() + wait(5) + repeat + if not LastInteractedPart then continue end + local part = LastInteractedPart.part + if part and part.Parent and _G.Methods.playerOwnsPart(game.Players.LocalPlayer,part) then + _G.Events.Notification:Fire("Auto Saving","Attempting Autosave") + CurrentSaveData = _G.Methods.saves.modelToData(part) + _G.Remotes.Save:InvokeServer(CurrentSaveData,"autosave",userid) + _G.Events.Save:Fire(CurrentSaveData) + + local selection_template = Instance.new("SelectionBox") + for index,item in pairs({ + SurfaceTransparency = 0.75, + SurfaceColor3 = Color3.fromRGB(231, 72, 183), + LineThickness = 0, + }) do + selection_template[index] = item + end + + _G.Methods.forEachConnectedObject(part,function(model) + wait() + local selection = selection_template:Clone() + selection.Adornee = model + selection.Parent = model + Debris:AddItem(selection,0.1) + end) + + update_save_info(function() + save_info["autosave"] = tostring(os.date("%c",os.time())) + end) + + local model = LoadModel("autosave",userid) + slots["autosave"].model = model + saves["autosave"] = model + end + until not wait(60*2.5) + end) + end + end) + return section,saves +end + +_G.Events.Create.Event:Connect(function(part) + LastInteractedPart = part +end) + +local function RenderStepped(delta) -- General procedure for allowing us to pick a part from a vehicle to later select (per frame) + if CurrentPlacementConfig then return end + local result = _G.Methods.getMouseHit() + if result and result.Instance then + local part + if _G.Methods.playerOwnsPart(_G.Player,result.Instance) then + part = result.Instance + elseif _G.Methods.playerOwnsPart(_G.Player,result.Instance.Parent) then + part = result.Instance.Parent + end + SelectionBox.Adornee = part + CurrentSaveRoot = part + end +end + +function InitTool(script) + SelectionBox = script:WaitForChild("SelectionBox") + + local renderSteppedEvent = nil + + script.Parent.Activated:Connect(function() + if CurrentSaveRoot then + local root = CurrentSaveRoot + + OffloadSelection() + + CurrentSaveData = _G.Methods.saves.modelToData(root) + + local selection_template = Instance.new("SelectionBox") + for index,item in pairs({ + SurfaceTransparency = 0.75, + SurfaceColor3 = Color3.fromRGB(154, 46, 231), + LineThickness = 0 + }) do + selection_template[index] = item + end + + _G.Methods.forEachConnectedObject(root,function(model) + local selection = selection_template:Clone() + selection.Adornee = model + selection.Parent = model + table.insert(SelectionBoxes,selection) + end) + end + end) + script.Parent.Unequipped:Connect(function() + OffloadSelection() + SaveFrame.Visible = false + SelectionBox.Adornee = nil + if renderSteppedEvent then renderSteppedEvent:Disconnect() end + end) + + script.Parent.Equipped:Connect(function() + SaveFrame.Visible = true + if renderSteppedEvent then renderSteppedEvent:Disconnect() end + renderSteppedEvent = game:GetService("RunService").RenderStepped:Connect(RenderStepped) + end) +end + +local player_saves +function InitUI(script) + SaveFrame = script.Parent + Stroke = script:WaitForChild("UIStroke") + SaveData = script.Parent:WaitForChild("SaveData") + SaveTabs = script.Parent:WaitForChild("SaveTabs") + SaveLists = script.Parent:WaitForChild("SaveLists") + Templates = script.Parent:WaitForChild("Template") + + SaveFrame.Visible = false + Templates.Frame.Visible = false + + PlayerSaveList,player_saves = CreateUserSection(_G.UserId,true) + for i = 1,2 do + local SubSlot = PlayerSaveList:CreateSlot() + SubSlot.order = string.format("SUB %i",i) + SubSlot.name = string.format("Subassembly %i",i) + SubSlot.on_save_clicked = function() + if (not CurrentSaveData) or (#CurrentSaveData == 0) then + _G.Events.Notification:Fire("Vehicle Save Failed","Please select a vehicle to save") + return + end + SubassemblyData = CurrentSaveData + SubassemblyModel = _G.Methods.saves.dataToModel(CurrentSaveData,false) + SubSlot.model = SubassemblyModel + OffloadSelection() + end + SubSlot.on_mirror_clicked = function() + OffloadSelection() + if not SubassemblyData then return end + if SubassemblyModel then SubassemblyModel:Destroy() end + SubassemblyModel = _G.Methods.saves.dataToModel(SubassemblyData,false,true) + SubSlot.model = SubassemblyModel + end + SubSlot.on_load_clicked = function() + OffloadSelection() + if not SubassemblyModel then return end + CurrentPlacementConfig = Placement.begin(SubassemblyModel:Clone(),{ + confirm = function(config) + _G.Events.Notification:Fire("Subassembly Placing!") + _G.Remotes.PlaceSubassembly:InvokeServer(config.model.PrimaryPart.CFrame,SubassemblyData,config.touching) + config:stop() + end, + stopped = function(config) + config.model:Destroy() + CurrentPlacementConfig = nil + end, + }) + end + SubSlot:show() + end + CreateUserSection("public",_G.Remotes.Admin:InvokeServer()) +end + +Commands.new("open save (.+)","open save : Opens saves in your tool.",function(userid) + CreateUserSection(userid,false) + _G.Events.Notification:Fire("Opened Read-Only Save",userid) +end) +Commands.new("open write save (.+)","open write save : Opens write saves in your tool.",function(userid) + CreateUserSection(userid,true) + _G.Events.Notification:Fire("Opened Write Save",userid) +end) +Commands.new("export save (.+) (.+)","export save : Prints out your save",function(userid,key) + _G.Events.Notification:Fire("Save Export",tostring(_G.Remotes.Load:InvokeServer(userid,key))) +end) +Commands.new("show indices","show indices: Prints out save indices",function() + local result = "" + local indices = _G.SaveIndices + for userid,tab in pairs(indices) do + result = result .. "Userid: "..tostring(userid).."\n" + for index,item in pairs(tab) do + result = result .. " "..tostring(index)..": "..tostring(item).."\n" + end + end + print(result) + _G.Events.Notification:Fire("Save Indicies",result) +end) + +return getfenv() \ No newline at end of file diff --git a/scripts/modules/SeatOwnership.lua b/scripts/modules/SeatOwnership.lua new file mode 100644 index 0000000..271256e --- /dev/null +++ b/scripts/modules/SeatOwnership.lua @@ -0,0 +1,30 @@ +local module = {} + +local function returnNetworkOwner(this) + local owner = this:GetAttribute("NetworkOwner") or this.Parent:GetAttribute("NetworkOwner") + if owner then owner = game.Players:GetPlayerByUserId(owner) end + this:SetNetworkOwner(owner) -- there is a bug here? +end + +function module.init(this) + do return end + this.Parent.Changed:Connect(function(name) + if name == "Occupant" then + if this.Parent.Occupant then + local player = game.Players:GetPlayerFromCharacter(this.Parent.Occupant.Parent) + if player then + if this.Parent.Parent:HasTag("P_"..player.UserId) or true then + this.Parent:SetNetworkOwner(player) + --print("Vehicle ownership set to "..player.DisplayName) + end + else + returnNetworkOwner(this.Parent) + end + else + returnNetworkOwner(this.Parent) + end + end + end) +end + +return module diff --git a/scripts/modules/SerialisationModule.lua b/scripts/modules/SerialisationModule.lua new file mode 100644 index 0000000..c26c2cf --- /dev/null +++ b/scripts/modules/SerialisationModule.lua @@ -0,0 +1,409 @@ +local partStateModule = require(script.Parent.PartStateModule) +local saves = {} +local id_parts = {} +local parts_loaded +spawn(function() + game.ReplicatedStorage:WaitForChild("Parts") + local function added(object) + if object.Parent:IsA("Folder") then + local id = object:GetAttribute("PartId") + if id then id_parts[id] = object end + end + end + for _,object in pairs(_G.Storage.Parts:GetDescendants()) do + added(object) + end + _G.Storage.Parts.DescendantAdded:Connect(added) + wait(1) + parts_loaded = true +end) +do + local pairs = pairs + local ipairs = ipairs + local isa = Instance.new("Part").IsA + local no_cframe = {} + local cframe_of = function(object) + if isa(object,"BasePart") then + return object.CFrame + elseif isa(object,"Model") then + return (object.PrimaryPart or no_cframe)["CFrame"] + end + end + local cframe_set = function(object,cframe) + if isa(object,"Model") then + object:SetPrimaryPartCFrame(cframe) + elseif isa(object,"BasePart") then + object.CFrame = cframe + end + end + local weld_is_build = {["BuiltWeldConstraint"] = true} + local insert = table.insert + local typeof = typeof + local is_table = {["table"]=true} + local id_module = _G.Methods.Ids + local colorPart = _G.Methods.colorPart + local ts = tostring + local tn = tonumber + local format = string.format + local concat = table.concat + local cframe_new = CFrame.new + local vector3_new = Vector3.new + local table_create = table.create + local form_triangle = partStateModule.form_triangle + local get_nodes = partStateModule.get_nodes + local apply_nodes = partStateModule.apply_nodes + local apply_shift = partStateModule.apply_shift + local apply_size = partStateModule.apply_size + local apply_material = partStateModule.apply_material + local apply_internal_welds = partStateModule.apply_internal_welds + local hexxie = require(script.Parent.HexxieModule) + local hd = hexxie.decode + local he = hexxie.encode + local hexxie_cframe_match = string.rep("(...........)",12) + local hexxie_position_match = string.rep("(...........)",3) + local default_cframe_format = string.rep("%s,",10).."%s" + local id_attach = {} + local compress_cframe_decode = function(s) + if not s then return nil end + local a,b,c,d,e,f,g,h,i,j,k,l = s:match(hexxie_cframe_match) + return cframe_new(hd(a),hd(b),hd(c),hd(d),hd(e),hd(f),hd(g),hd(h),hd(i),hd(j),hd(k),hd(l)) + end + local compress_cframe_encode = function(c) + local a,b,c,d,e,f,g,h,i,j,k,l = c:GetComponents() + return concat({he(a),he(b),he(c),he(d),he(e),he(f),he(g),he(h),he(i),he(j),he(k),he(l)}) + end + local compress_position_decode = function(s) + local a,b,c = s:match(hexxie_position_match) + return vector3_new(hd(a),hd(b),hd(c)) + end + local compress_position_encode = function(v) + return concat({he(v.X),he(v.Y),he(v.Z)}) + end + local position + local http = game:GetService("HttpService") + local http_json_encode = http.JSONEncode + local http_json_decode = http.JSONDecode + local json_decode = function(value) + return http_json_decode(http,value) + end + local json_encode = function(value) + return http_json_encode(http,value) + end + local ID_INDEX = 1 + local CFRAMES_INDEX = 2 + local EDIT_INDEX = 3 + saves["4"] = { + decode = function(raw,options) + --print(raw) + repeat wait() until parts_loaded + local final = {errors = {}} + local function err(text) + warn(text) + table.insert(final.errors,text) + end + options = options or {} + local _compress_cframe_decode = compress_cframe_decode + local _compress_position_decode = compress_position_decode + if options.mirror then + _compress_cframe_decode = function(str) + local cf = compress_cframe_decode(str) + local x,y,z = cf:ToEulerAnglesYXZ() + return CFrame.fromEulerAnglesYXZ(x,-y,-z)+Vector3.new(cf.X,-cf.Y,cf.Z) + end + _compress_position_decode = function(str) + local pos = compress_position_decode(str) + return vector3_new(-pos.X,pos.Y,pos.Z) + end + end + local compressed = raw:match("\"compress\"%:true") + if compressed then raw = hexxie.rle_decode(raw) end + local data = _G.Http:JSONDecode(raw) + _G.Union(options,data.options or {}) + local physics = options.physics + local template_cframe = Instance.new("CFrameValue") + local weld_template = Instance.new("WeldConstraint") + local built_weld_template = weld_template:Clone() + built_weld_template.Name = "BuiltWeldConstraint" + local model = Instance.new("Model") + final.model = model + local parts = {} + local editActions = { + c = function(edit,part) + local color = Color3.new(unpack(edit)) + colorPart(part,color) + end, + h = function(edit,part) + local hinge = part:FindFirstChild("Hinge") + if hinge then hinge.CFrame = compress_cframe_decode(edit) end + end, + r = function(edit,part) + local slider = part:FindFirstChild("Slider") + if slider then slider.CFrame = compress_cframe_decode(edit) end + end, + m = function(edit,part) + if edit then apply_material(part,Enum.Material[edit]) end + end, + s = function(edit,part) + apply_size(part,Vector3.new(unpack(edit))) + end, + S = function(edit,part) + apply_shift(part,edit) + end, + } + local poly_to_internally_weld = {} + local data_cframes = data.cframes + local data_edits = data.edits + for id,object in pairs(data.objects) do + local template = id_parts[object[ID_INDEX]] + if not template then err("Missing part with id "..tostring(id)) continue end + local instance = template:Clone() + instance.Parent = model + parts[id] = instance + + local nodes = {} + local transform_ids = object[CFRAMES_INDEX] + -- BEGIN PARITY 4.1 (CAN BE REMOVED LATER) + if typeof(transform_ids) ~= "table" then transform_ids = {transform_ids} end + -- END PARITY 4.1 + if #transform_ids == 1 then + insert(nodes,compress_cframe_decode(data_cframes[transform_ids[1]])) + elseif #transform_ids == 2 then + insert(nodes,compress_cframe_decode(data_cframes[transform_ids[1]])) + insert(nodes,hd(data_cframes[transform_ids[2]])) + else + for _,id in ipairs(transform_ids) do + insert(nodes,compress_position_decode(data_cframes[id])) + end + insert(poly_to_internally_weld,instance) + end + apply_nodes(instance,nodes) + -- Edit part + if object[EDIT_INDEX] then + for _,edit_id in pairs(object[EDIT_INDEX]) do + local key,value = unpack(data_edits[edit_id]); + (editActions[key] or function(value,instance) + local edit = instance:FindFirstChild(key) + if not edit then + err(string.format("No key %s for %s",key,instance:GetFullName())) + else + if typeof(value) == "table" then value = _G.Http:JSONEncode(value) end + edit.Value = value + end + end)(value,instance) + end + end + -- Change anchoring + if not options.physics then + if instance:IsA("Model") then + instance.PrimaryPart.Anchored = true + else + instance.Anchored = true + end + end + end + if options.physics then + for _,weld in pairs(data.welds) do + local w = built_weld_template:Clone() + local at,bt = unpack(weld) + local ap = parts[at[1]] + local bp = parts[bt[1]] + w.Parent = ap + if ap:IsA("Model") then + if at[2] then + ap = ap:FindFirstChild(at[2]) or ap.PrimaryPart + else + ap = ap.PrimaryPart + err("Missing subpart reference for weld on model A") + end + end + if bp:IsA("Model") then + if bt[2] then + bp = bp:FindFirstChild(bt[2]) or bp.PrimaryPart + else + bp = bp.PrimaryPart + err("Missing subpart reference for weld on model B") + end + end + w.Part0 = ap + w.Part1 = bp + end + for _,poly in ipairs(poly_to_internally_weld) do + apply_internal_welds(poly) + end + else + for _,part in pairs(model:GetDescendants()) do + if part:IsA("BasePart") then part.CanCollide = false part.Anchored = true end + end + end + local root = parts[1] + if not root then error("Fatal: Missing root part; implying #parts = 0") end + if root:IsA("Model") then root = root.PrimaryPart end + model.PrimaryPart = root + final.model = model + return final + end, + encode = function(root,options) + repeat wait() until parts_loaded + local final = {errors = {}} + local function err(text) + warn(text) + table.insert(final.errors,text) + end + id_module.forEachConnectedObject(root,function(object) + if object:IsA("VehicleSeat") then root = object end + end) + final.root = root + + if not options then options = {} end + + local origin = cframe_of(root) + if not origin then err("Root CFrame could not be encoded, it is probably broken") return final end + + local ids = {} + + local cframes = {} + local edits = {} + local welds = {} + local objects = {} + local data = { + options = options, + objects = objects, + cframes = cframes, + edits = edits, + welds = welds + } + + local should_optimise_cframe = not not options.optimise_cframe + local cframe_epsilon = options.cframe_epsilon or 0.1 + local optimise_edit = {} + local optimise_cframe = {} + + local function cframe_id(c) + local existing = optimise_cframe[c] + if not existing then + insert(cframes,c) + local len = #cframes + optimise_cframe[c] = len + return len + else + return existing + end + end + + local function edit_id(key,value) + local str + if type(value) == "table" then + str = json_encode(value) + elseif type(value) ~="string" then + str = tostring(value) + else + str = value + end + local tab = optimise_edit[key] + if not tab then tab = {} optimise_edit[key] = tab end + if tab[str] then + return tab[str] + else + insert(edits,{key,value}) + tab[str] = #edits + return #edits + end + end + + local errors = {} + + local index = 0 + id_module.forEachConnectedObject(root,function(object) + index = index + 1 + ids[object] = index + + local id = object:GetAttribute("PartId") or object.Name + if not id then insert(errors,string.format("Part without an ID! %s",ts(object))) end + + -- ENCODE TRANSFORM + local transform = get_nodes(object) + if #transform >= 3 then + for index,position in pairs(transform) do + transform[index] = cframe_id(compress_position_encode(origin:PointToObjectSpace(position))) + end + elseif #transform == 2 then + transform[1] = cframe_id(compress_cframe_encode(origin:ToObjectSpace(transform[1]))) + transform[2] = cframe_id(he(transform[2])) + elseif #transform == 1 then + transform[1] = cframe_id(compress_cframe_encode(origin:ToObjectSpace(transform[1]))) + end + + -- ENCODE EDITS + local edits = {} + for index,edit in pairs(object:GetChildren()) do + if edit:HasTag("Edit") or edit:HasTag("EditHidden") then + if edit:HasTag("JSON") then + insert(edits,edit_id(edit.Name,json_decode(edit.Value))) + else + local value = edit.Value + if (edit:GetAttribute("Default") ~= value) then + insert(edits,edit_id(edit.Name,value)) + end + end + end + end + + -- ENCODE CANDID DISPLACEMENT + local hinge = object:FindFirstChild("Hinge") + if hinge then insert(edits,edit_id("h",compress_cframe_encode(origin:ToObjectSpace(hinge.CFrame)))) end + local slider = object:FindFirstChild("Slider") + if slider then insert(edits,edit_id("r",compress_cframe_encode(origin:ToObjectSpace(slider.CFrame)))) end + + -- ENCODE PROPERTIES + local color = object:GetAttribute("Color") -- TODO HEXXIE OPTIMISE COLOUR AND SIZE + if color then insert(edits,edit_id("c",{color.R,color.G,color.B})) end + local material = object:GetAttribute("Material") + if material then insert(edits,edit_id("m",material)) end + local size = object:GetAttribute("Size") + if size then insert(edits,edit_id("s",{size.X,size.Y,size.Z})) end + local shift = object:GetAttribute("Shift") + if shift then insert(edits,edit_id("S",shift)) end + + -- INSERT OBJECT + local data = {id,transform} + if #edits > 0 then insert(data,edits) end + + insert(objects,data) + end) + + -- ENCODE WELDS + for object,id in pairs(ids) do + for _,weld in pairs(object:GetDescendants()) do + if weld_is_build[weld.Name] then + if weld.Part0 and weld.Part1 then + local first = {ids[weld.Part0]} + if not first[1] then + first[1] = ids[weld.Part0.Parent] + first[2] = weld.Part0.Name + if not first[1] then err("There was a stray first weld! "..tostring(weld:GetFullName())) continue end + end + local second = {ids[weld.Part1]} + if not second[1] then + second[1] = ids[weld.Part1.Parent] + second[2] = weld.Part1.Name + if not second[1] then err("There was a stray second weld! "..tostring(weld:GetFullName())) continue end + end + insert(data.welds,{first,second}) + else + err("There was a dead weld!"..tostring(weld:GetFullName())) + end + end + end + end + + local json = _G.Http:JSONEncode(data) + local result = json + if options.compress then result=hexxie.rle_encode(json) end + local check = hexxie.rle_decode(result) + result = "v4save"..result + final.data = result + return final + end, + } +end +return saves \ No newline at end of file diff --git a/scripts/modules/TerrainModule.lua b/scripts/modules/TerrainModule.lua new file mode 100644 index 0000000..d1300c1 --- /dev/null +++ b/scripts/modules/TerrainModule.lua @@ -0,0 +1,143 @@ +local is_client = game.Players.LocalPlayer +if is_client then return {} end +local scale = 20 -- * 10 +local generation_folder = game.Workspace.Generation +local function triangle(p1,p2,p3,r1,r2) -- simple function, might work? + local s = 0.01 + local bottom = { + b1 = nil, + b2 = nil, + dist = 0, + other = nil + } + local function check(point1,point2,other) + local dist = (point1 - point2).Magnitude + if dist > bottom.dist then + bottom.b1 = point1 + bottom.b2 = point2 + bottom.other = other + bottom.dist = dist + end + end + check(p1,p2,p3) -- check all of the different options for bottom and get the right one? + check(p1,p3,p2) + check(p2,p3,p1) + local view = CFrame.lookAt(bottom.b1,bottom.b2,(bottom.other-bottom.b1).Unit) + local object = view:PointToObjectSpace(bottom.other) + local length = object.Z + local height = object.Y + r1.Size = Vector3.new(0.25,height-s,-length-s) + r1.CFrame = (view * CFrame.Angles(0,math.rad(180),0)) * CFrame.new(0,height/2+s/2,-length/2+s/2) + r2.Size = Vector3.new(0.25,height-s,-(-bottom.dist-length)-s) + r2.CFrame = view * CFrame.new(0,height/2+s/2,-(bottom.dist-length)/2+s/2) + --[[if r1.Position.Y < 0 then + r1.Color = Color3.new(0.764706, 0.756863, 0.478431) + r1.Material = Enum.Material.Sand + end + if r2.Position.Y < 0 then + r2.Color = Color3.new(0.764706, 0.756863, 0.478431) + r2.Material = Enum.Material.Sand + end]] + return r1,r2 +end +local editable_mesh_template = Instance.new("EditableMesh") +local editable_mesh_add_vertex = editable_mesh_template.AddVertex +local editable_mesh_add_triangle = editable_mesh_template.AddTriangle +local ta +local tb +local tc +local function ntri(mesh,a,b,c,d) + ta = editable_mesh_add_vertex(mesh,a) + tb = editable_mesh_add_vertex(mesh,b) + tc = editable_mesh_add_vertex(mesh,c) + editable_mesh_add_triangle(mesh,ta,tb,tc) + ta = editable_mesh_add_vertex(mesh,a) + tb = editable_mesh_add_vertex(mesh,c) + tc = editable_mesh_add_vertex(mesh,d) + editable_mesh_add_triangle(mesh,ta,tb,tc) +end +local function part() + local part = Instance.new("Part") + part.Parent = generation_folder + part.Anchored = true + return part +end +local function tri(mat) + local new = mat:Clone() + new.Parent = generation_folder + return new +end +local fidelity = 20 +local tree = _G.Storage.Tree +local mat_sand = game.Workspace:WaitForChild("SandTriangle") +local mat_grass = game.Workspace:WaitForChild("GrassTriangle") +local tri_union_pool = {} +local function unionTable(tbl) + if #tbl > 2 then + local start = tbl[#tbl] + table.remove(tbl,#tbl) + start:UnionAsync(tbl).Parent = game.Workspace + for _,item in pairs(tbl) do + item:Destroy() + end + print("union!") + end +end +local function chunk(sx,sy,ox,oy) + --[[for i=18,2,-4 do + local water = _G.Storage.Water:Clone() + water.Size = Vector3.new(sx,0,sy)*scale*scale + water.Position = Vector3.new(ox*sx+sx/2,-2,oy*sy+sy/2)*scale + Vector3.new(0,i,0) + water.Parent = generation_folder + end]] + --[[local object = Instance.new("MeshPart") + object.Parent = generation_folder + object.Name = "MeshObject" + object.Anchored = true + object.Material = Enum.Material.Grass + object.Color = Color3.new(0.254902, 0.403922, 0.133333) + object.Size = Vector3.new(1,1,1) + local edit = Instance.new("EditableMesh") + edit.Parent = object + edit.Name = "VertexObject" + local rows = {} + local last_column = nil + local sumtris = {} + for x=ox*sx,ox*sy+sx,fidelity do + local columns = {} + table.insert(rows,columns) + local last_index = 1 + for y=oy*sy,oy*sy+sy,fidelity do + local biome = math.clamp(math.noise(x/1000,y/1000,-6.432),0,1)*50 + local island = math.clamp(math.noise(x/200,y/200,-2.132)-0.3,0,1)*30 + local height = math.noise(x/10,y/10,0.123) + math.clamp(math.noise(x/50,y/50,0.123),0.2,1)*3 - (0.2-math.clamp(math.abs(math.noise(x/50,y/50,5.213)),0,0.2))*10 + island - 8 + biome + table.insert(columns,Vector3.new(x,height*2,y)) + if last_column and last_index > 1 and height > -5 then + local mat = mat_grass + local current = columns[last_index]*scale + local left = columns[last_index-1]*scale + local up_left = last_column[last_index-1]*scale + local up = last_column[last_index]*scale + --ntri(edit,current,up,up_left,left) + local t1,t2 = triangle(current,up,up_left,tri(mat),tri(mat)) + local t3,t4 = triangle(current,left,up_left,tri(mat),tri(mat)) + table.insert(sumtris,t1) + table.insert(sumtris,t2) + table.insert(sumtris,t3) + table.insert(sumtris,t4) + end + last_index = last_index + 1 + end + last_column = columns + end + task.delay(0,function()unionTable(sumtris)end)]] +end + +for x=-6,6 do + for y=-6,6 do + wait() + chunk(100,100,x,y) + end +end + +return {} \ No newline at end of file diff --git a/scripts/modules/VehicleSaveModule.lua b/scripts/modules/VehicleSaveModule.lua new file mode 100644 index 0000000..f4098ad --- /dev/null +++ b/scripts/modules/VehicleSaveModule.lua @@ -0,0 +1,3 @@ +local module = {} + +return module diff --git a/scripts/modules/objects/Aero.lua b/scripts/modules/objects/Aero.lua new file mode 100644 index 0000000..79a4963 --- /dev/null +++ b/scripts/modules/objects/Aero.lua @@ -0,0 +1,126 @@ +local module = {} + +local debugWings = game.Workspace.DebugWings.Value + +local space = game.Workspace.Objects +local dens = game.Workspace.AirDensity * -game.Workspace.CompensateDensity.Value +local ndelta = 1/60 +local function drag(v,r) + return v * r +end +local rnd = function(vec) return Vector3.new(math.round(vec.X),math.round(vec.Y),math.round(vec.Z)) end +local a = math.abs +local ln = math.log +local debwings = {} +local function deb(wing,force) + local part = debwings[wing] + if not part then + part = Instance.new("Part") + part.Name = "WingDebug" + part.Parent = game.Workspace.Hidden + part.Color = Color3.new(0.152941, 0.764706, 0.0588235) + part.Material = Enum.Material.Neon + part.Anchored = true + part.CanCollide = false + part.Transparency = 0.8 + part.Size = Vector3.new(0.25,0.25,2) -- we determine the wing's strength and do math by its size, the bigger, the more lift + debwings[wing] = part + end + part.Transparency = 0.5 + part.CFrame = CFrame.lookAt(wing.Position,wing.Position+force) + part.Size = Vector3.new(0.25,0.25,force.Magnitude) + part.CFrame = part.CFrame + part.CFrame.LookVector * part.Size.Z * 0.5 +end + +require(game.ReplicatedFirst.ReadyModule)("WingCommands") +local process = true + +game.Workspace.DebugWings.Changed:Connect(function() + debugWings = game.Workspace.DebugWings.Value + if not debugWings then + for _,item in pairs(debwings) do + item:Destroy() + end + debwings = {} + end +end) +game.Workspace.CompensateDensity.Changed:Connect(function() + dens = game.Workspace.AirDensity * -game.Workspace.CompensateDensity.Value +end) + +function module.new(this,wing,context) + --local mul = wing:GetAttribute("AeroMultiplier") or 1 + this.bounds = {} + this.volume = {} + if wing:IsA("Model") then + this.aero = {} + for _,part in pairs(wing:GetChildren()) do + if part:HasTag("IsAerodynamic") then + table.insert(this.aero,part) + end + end + else + this.aero = {wing} + end + for _,part in pairs(this.aero) do + local s = part.Size + this.bounds[part] = Vector3.new(s.Y*s.Z,s.X*s.Z,s.Y*s.X)-- * mul + this.volume[part] = s.X*s.Y*s.Z + end + return this +end + +local visc = 0.75 +local commands = require(_G.Modules.CommandsModule) +commands.new("viscosity (.+)","viscosity : change air viscosity coefficient",function(n) + visc = tonumber(n) +end) + +module.run = @native +function(this,wing,context) + local bounds = this.bounds + local volume = this.volume + if not process then return true end + if not this.aero then print(this,wing) end -- BECAUSE WHAT THE BLOODY HELL IS THIS!?!?? + for _,wing in pairs(this.aero) do + --if not (wing.Parent == space or wing.Parent.Parent == space) and v.player then return end + local s = bounds[wing] + if not s then print(this.aero,bounds,wing,s) end + local p = wing.Position + local v = wing:GetVelocityAtPosition(p) -- velocity of wing + local d = dens * 20 + local fmass = 2 -- mass of air generally + --[[if p.Y < 0 then + d = d * 5 + fmass = 100 -- mass of water + end]] + v = v - Vector3.new(0, + 0--0.15 * v.Magnitude -- implement upthrust due to pressure difference across an airfoil (lift) + ,0) + local lv = wing.CFrame.Rotation:PointToObjectSpace(v) -- velocity for each wing face + --mv = lv.Magnitude + 0.0001 -- small number to stop zero error + + -- "math", YEAH REALLY? i didnt REALISE! I don't even know what this does... + --lp = Vector3.new(lv.X/(a(scale.X*d*lv.X)+1)-lv.X,lv.Y/(a(scale.Y*d*lv.Y)+1)-lv.Y,lv.Z/(a(scale.Z*d*lv.Z)+1)-lv.Z) -- math + local drag = Vector3.new(lv.X*s.X,lv.Y*s.Y,lv.Z*s.Z) + -- implement anti-viscosity (may the riemann sum gods be with the low end machines + local lp = (drag) * context.delta*d*math.tanh(v.Magnitude*0.0075) + + local r = (wing.CFrame.Rotation:PointToWorldSpace(lp)) + + r = r + Vector3.yAxis * fmass * volume[wing] * 2 * context.delta -- implement upthrust due to pressure difference in a fluid column (floating) + + wing:ApplyImpulseAtPosition(r,wing.Position) + + if debugWings then deb(wing,r) end + end + return true +end + +module.manifest = { + predicate = function(instance) + return instance:HasTag("IsAerodynamic") or instance.Name:match("Sheet") -- LAST CONDITION IS NOT NEEDED + end, +} + +return module diff --git a/scripts/modules/objects/Balloon.lua b/scripts/modules/objects/Balloon.lua new file mode 100644 index 0000000..72cb1cd --- /dev/null +++ b/scripts/modules/objects/Balloon.lua @@ -0,0 +1,34 @@ +local module = {} + +local cool_rate = 0.25 +function module.new(this,balloon,context) + this.heat = 0 +end + +function module.run(this,balloon,context) + this.heat = this.heat * (1 - cool_rate * context.delta) -- wrong but idc + local force = Vector3.new(0,1,0) * (1-(1/(1+this.heat))) * 1000 * context.delta * balloon.Size.Magnitude + balloon:ApplyImpulseAtPosition(force,balloon.Position) + context.replicate(this,balloon,module,this.heat) + return true +end + +function module.replicate(this,balloon,context,heat) + this.heat = heat +end + +module.update = module.new + +module.messages = { + flux = function(this,balloon,context,heat) + this.heat = this.heat + heat + end, +} +module.messages.default = nil + +module.manifest = { + "LongBalloon", + "RoundBalloon" +} + +return module \ No newline at end of file diff --git a/scripts/modules/objects/Boiler.lua b/scripts/modules/objects/Boiler.lua new file mode 100644 index 0000000..e482d56 --- /dev/null +++ b/scripts/modules/objects/Boiler.lua @@ -0,0 +1,54 @@ +local module = {} + +local function burn_effect(this,boiler,context,burn) + for index,item in pairs(this.burning) do + item.Color = Color3.fromRGB(255, 137, 73):Lerp(Color3.fromRGB(115, 61, 33),1-math.abs(burn)) + end +end + +function module.new(this,boiler,context) + this.burning = {} + this.scale = boiler:GetExtentsSize().Magnitude + this.exhaust = _G.GetInstance(boiler,"Exhaust") + for index,item in pairs(boiler:GetChildren()) do + if item.Name == "Fire" then + table.insert(this.burning,item) + end + end + return this +end + +function module.idle(this,boiler) return true end + +local begin = tick() +module.run = function(this,boiler,context) + local throttle = this.throttle or context.controls[_G.GetValue(boiler,"Binding","throttle")] or this.Throttle or 0 + context.replicate(this,boiler,module,throttle) + local objects = {} + context.connected("ConnectorFume","Exhaust",this.exhaust,function(object) + table.insert(objects,object) + end) + for index,object in pairs(objects) do + context.message(object,"flux",this.scale * math.abs(throttle) * context.delta * (1/#objects)) + end + return true +end + +module.replication_delta = 0.25 +function module.replicate(this,boiler,context,burn) + burn_effect(this,boiler,context,burn) +end + +module.messages = { + control = function(this,object,context,value) + this.throttle = value + end, +} +module.messages.default = module.messages.control + +module.manifest = { + "Boiler1", + "Boiler2" +} + +return module diff --git a/scripts/modules/objects/Boombox.lua b/scripts/modules/objects/Boombox.lua new file mode 100644 index 0000000..ebcb36d --- /dev/null +++ b/scripts/modules/objects/Boombox.lua @@ -0,0 +1,26 @@ +local module = {} + +function module.new(this,boombox,context) + this.sound = _G.GetInstance(_G.GetInstance(boombox,"Plank"),"Sound") +end + +module.messages = { + source = function(this,speaker,context,value) + this.sound.SoundId = "rbxassetid://"..(tostring(value or "") or "") + end, + default = function(this,speaker,context,value) + if value > 0.5 then + this.sound:Play() + elseif value == 0 then + this.sound:Pause() + elseif value < -0.5 then + this.sound:Stop() + end + end, +} + +module.manifest = { + "Boombox" +} + +return module \ No newline at end of file diff --git a/scripts/modules/objects/Cable.lua b/scripts/modules/objects/Cable.lua new file mode 100644 index 0000000..f3f2fdb --- /dev/null +++ b/scripts/modules/objects/Cable.lua @@ -0,0 +1,19 @@ +local module = {} + +local partStateModule = require(_G.Modules.PartStateModule) +function module.new(this,cable,context) + +end + +function module.run(this,cable,context) + if context.frequency(this,"cable_material",1) then cable.Material = Enum.Material.Fabric end + return true +end + +module.update = module.new + +module.manifest = { + "DataCable" +} + +return module \ No newline at end of file diff --git a/scripts/modules/objects/Computer.lua b/scripts/modules/objects/Computer.lua new file mode 100644 index 0000000..3907e15 --- /dev/null +++ b/scripts/modules/objects/Computer.lua @@ -0,0 +1,59 @@ +local module = {} + +local programModule = require(_G.Modules.ProgramModule) + +function module.new(this,computer,context) + this.value = _G.GetInstance(computer,"Program") + this.led = _G.GetInstance(computer,"LED") + this.program = programModule.open_program(this.value,nil) + this.program:load() + this.inputs = {} + this.output = function(pin,name,value) + context.connected("ConnectorData","ConnectorEnd",computer[pin],function(object) + context.message(object,name,value) + end) + end + this.input = function(name) + return this.inputs[name] + end + this.control = function(name) + return context.controls[name] + end + this.message = function(title,value) + if context.queue(this,"notification",3) then _G.Events.Notification:Fire(title,value) end + end + this.led.Color = Color3.fromRGB(255,255,255) + return this +end + +local begin = tick() +module.run = _G.Protect(function(this,computer,context) + if not this.program then this.led.Color = Color3.fromRGB(255,0,0) return true end + this.led.Color = Color3.fromRGB(0,255,0) + this.program.variables = this.variables or {} + this.program:run(this) + context.replicate(this,computer,module,this.program.variables) + return true +end) + +function module.remove(this,computer,context) + +end + +module.replication_delta = 0.25 +function module.replicate(this,computer,context,variables) + this.variables = variables +end + +module.messages = { + default = function(this,object,context,value) + local pin = object.Name + this.inputs[pin] = value + end +} + +module.manifest = { + "Microcontroller" +} + +return module diff --git a/scripts/modules/objects/Disconnector.lua b/scripts/modules/objects/Disconnector.lua new file mode 100644 index 0000000..473ab30 --- /dev/null +++ b/scripts/modules/objects/Disconnector.lua @@ -0,0 +1,44 @@ +local module = {} + +function module.new(this,disconnector,context) + this.grip = _G.GetInstance(disconnector,"Grip") + this.binding = _G.GetInstance(disconnector,"Binding") + return this +end + +module.run = _G.Protect(function(this,disconnector,context) + if this.control or 0 < 0.5 and context.queue(this,module.name,0.4) then + context.replicate(this,disconnector,module) + elseif context.controls[this.binding.Value] or 0 < 0.5 then + context.replicate(this,disconnector,module) + end + return true +end) + +module.replication_delta = 0.25 +function module.replicate(this,disconnector,context) + if _G.IsClient then return end + for _,weld in pairs(this.grip:GetJoints()) do + if + weld:IsA("WeldConstraint") and + weld.Name == "BuiltWeldConstraint" and + weld.Parent + then + weld:Destroy() + end + end + this.grip.Transparency = 1 + task.delay(0.3,function() this.grip.Transparency = 0 end) +end + +module.messages = { + control = function(this,disconnector,context,value) + this.control = tonumber(value) + end, +} + +module.manifest = { + "Disconnector" +} + +return module diff --git a/scripts/modules/objects/Display.lua b/scripts/modules/objects/Display.lua new file mode 100644 index 0000000..96d3e13 --- /dev/null +++ b/scripts/modules/objects/Display.lua @@ -0,0 +1,23 @@ +local module = {} + +function module.new(this,display,context) + this.screen = _G.GetInstance(display,"Screen") + this.gui = _G.GetInstance(this.screen,"Gui") + this.text = _G.GetInstance(this.gui,"Label") + this.text.Text = "PSx Display Driver 1.0\nStandby for message..." +end + +module.messages = { + default = function(this,display,context,value) + this.text.Text = tostring(value) + end, + color = function(this,display,context,value) + pcall(function() this.text.TextColor3 = value end) + end, +} + +module.manifest = { + "Display" +} + +return module \ No newline at end of file diff --git a/scripts/modules/objects/Emitter.lua b/scripts/modules/objects/Emitter.lua new file mode 100644 index 0000000..ecbaf04 --- /dev/null +++ b/scripts/modules/objects/Emitter.lua @@ -0,0 +1,41 @@ +local module = {} + +function module.new(this,emitter,context) + + return this +end + +function module.idle(this,engine) return true end + +local begin = tick() +module.run = _G.Protect(function(this,emitter,context) + + return true +end) + +module.replication_delta = 0.25 +function module.replicate(this,emitter,context,details) + +end + +module.messages = { + control = function(this,object,context,value) + this.throttle = value + end, + starter = function(this,object,context,value) + this.starter = value + end, +} +module.messages.default = module.messages.control + +module.manifest = { + "Engine", + "Engine2", + "BigEngine", + "BigRadial", + "Radial", + "Boiler1", + "Boiler2" +} + +return module diff --git a/scripts/modules/objects/Engine.lua b/scripts/modules/objects/Engine.lua new file mode 100644 index 0000000..6d4df8b --- /dev/null +++ b/scripts/modules/objects/Engine.lua @@ -0,0 +1,121 @@ +local module = {} + +local function rpm(rot) + return (rot/(math.pi*2))*60 +end + +local function rot(hinge) + return hinge.CFrame.Rotation:PointToObjectSpace(hinge.AssemblyAngularVelocity).Magnitude +end + +function module.new(this,engine,context) + this.powered = _G.GetValue(engine,"Powered",true) + this.inverted = _G.GetInstance(engine,"Inverted") + this.speed = _G.GetInstance(engine,"Speed") + this.rotor = _G.GetInstance(engine,"Hinge") + this.hinge = _G.GetInstance(engine,"HingeConstraint") + this.rotor_sound = this.rotor:FindFirstChild("RotorSound") + this.starter_sound = this.rotor:FindFirstChild("Starter") + this.starter_ratio = engine:GetAttribute("StarterRatio") or 1 + if this.powered then + this.hinge.ActuatorType = Enum.ActuatorType.Motor + else + this.hinge.ActuatorType = Enum.ActuatorType.None + end + this.hinge.LimitsEnabled = false + this.torque = engine:GetScale() * 100000 + return this +end + +function module.idle(this,engine) return true end + +local begin = tick() +module.run = _G.Protect(function(this,engine,context) + local throttle = this.throttle or context.controls[_G.GetValue(engine,"Binding","throttle")] or this.Throttle or 0 + if this.inverted.Value then throttle = throttle * -1 end + local starter = math.abs(throttle) > 0.1 + local target_speed = this.speed.Value * throttle + local current_speed = rot(this.rotor) + local accel + if math.abs(current_speed) > this.speed.Value * 0.1 then + -- Running + starter = false + accel = 40 + else + -- Starting + accel = 40 * this.starter_ratio --this.torque * (math.clamp(math.noise(0.123,0.456,(begin-tick())*0.5),0,1) + 0.5) * 0.2 + end + this.hinge.MotorMaxAcceleration = accel + if math.abs(throttle) > 0.05 then + this.hinge.AngularVelocity = target_speed + else + this.hinge.AngularVelocity = 0 + end + if not this.torque then print(this) return false end + this.hinge.MotorMaxTorque = this.torque * ((engine:GetAttribute("Health") or 1) / (engine:GetAttribute("MaxHealth") or 1)) * (1/(this.speed.Value + 1)) * 60 + context.replicate(this,engine,module,{Throttle = throttle, Starter = starter}) + return true +end) + +local legal_detail = { + Throttle = true, + Mixture = true, + Temperature = true, + Oil = true, + Air = true, + Fuel = true, + Coolant = true, + Power = true +} +module.replication_delta = 0.25 +function module.replicate(this,engine,context,details) + for index,item in pairs(details) do + if legal_detail[index] then + this[index] = item + engine:SetAttribute(index,item) + end + end + local speed = math.abs(rot(this.rotor)) + if details.Starter then + -- Starter on + if this.starter_sound then if not this.starter_sound.Playing then this.starter_sound:Play() end end + if this.rotor_sound then if this.rotor_sound.Playing then this.rotor_sound:Pause() end end + elseif speed > this.speed.Value * 0.1 then + -- Running + if this.rotor_sound then + if not this.rotor_sound.Playing then this.rotor_sound:Play() end + _G.Print(engine,this.rotor_sound.PlaybackSpeed,speed,this.speed.Value/60) + this.rotor_sound.PlaybackSpeed = math.clamp(math.abs(speed/150) * 1.5,0.5,1.5) + this.rotor_sound.Volume = math.clamp(math.abs(speed/150) * 0.05 + 0.05,0,0.1) + end + if this.starter_sound then if this.starter_sound.Playing then this.starter_sound:Stop() end end + else + if this.rotor_sound then if this.rotor_sound.Playing then this.rotor_sound:Stop() end end + if this.starter_sound then if this.starter_sound.Playing then this.starter_sound:Stop() end end + end +end + +module.messages = { + control = function(this,object,context,value) + this.throttle = value + end, + starter = function(this,object,context,value) + this.starter = value + end, +} +module.messages.default = module.messages.control + +module.manifest = { + "Engine", + "Engine2", + "BigEngine", + "BigRadial", + "Radial", + "Jet", + "Motor", + "Motor1", + "BigSpinWheel", + "SpinWheel" +} + +return module diff --git a/scripts/modules/objects/Gun.lua b/scripts/modules/objects/Gun.lua new file mode 100644 index 0000000..546be8b --- /dev/null +++ b/scripts/modules/objects/Gun.lua @@ -0,0 +1,37 @@ +local module = {} + +local limit = 4 + +local bullet = require(_G.Modules.BulletModule) + +function module.new(this,gun,context) + this.barrel = _G.GetInstance(gun,"Barrel") + this.binding = _G.GetInstance(gun,"Binding") + this.rate = _G.GetValue(gun,"Rate",0.15) + this.last = tick() +end + +module.run = _G.Protect(function(this,gun,context) + if (this.control or context.controls[this.binding.Value] or 0) < 0.5 then return true end + if tick() - this.last < this.rate then return true end + -- Fire + bullet.fire(this.barrel) + this.last = tick() + this.barrel:ApplyImpulse(this.barrel.CFrame.RightVector * -500) + return true +end) + +module.fire_rate = 0.05 + +module.messages = { + control = function(this,gun,context,value) + this.control = value + end, +} + +module.manifest = { + "Gun", + "Cannon" +} + +return module diff --git a/scripts/modules/objects/Hinge.lua b/scripts/modules/objects/Hinge.lua new file mode 100644 index 0000000..a83b81a --- /dev/null +++ b/scripts/modules/objects/Hinge.lua @@ -0,0 +1,34 @@ +local module = {} + +function module.new(this,hinge,context) + this.hinge = _G.GetInstance(hinge,"HingeConstraint") + local function update() + if this.limited then this.hinge.LimitsEnabled = this.limited.Value end + if this.lower then this.hinge.LowerAngle = this.lower.Value end + if this.upper then this.hinge.UpperAngle = this.upper.Value end + end + if _G.IsServer then + this.limited = hinge:FindFirstChild("Limited") + this.upper = hinge:FindFirstChild("Upper") + this.lower = hinge:FindFirstChild("Lower") + if this.limited then this.limited.Changed:Connect(update) end + if this.lower then this.lower.Changed:Connect(update) end + if this.upper then this.upper.Changed:Connect(update) end + end + return this +end + +module.manifest = { + "FlatServoRoll", + "FlatServoPitch", + "Servo", + "ControlSheet", + "NanoControlSheet", + "MetalServo", + "Spinner", + "Hinge", + "Rotator", + "RotatorPlate" +} + +return module \ No newline at end of file diff --git a/scripts/modules/objects/Lever.lua b/scripts/modules/objects/Lever.lua new file mode 100644 index 0000000..573d1dc --- /dev/null +++ b/scripts/modules/objects/Lever.lua @@ -0,0 +1,45 @@ +local module = {} + +function position(motor,phase,phases,target,origin) + motor.C0 = origin + local alpha = (phase)/(phases-1) + motor.C1 = CFrame.new():Lerp(target,alpha) +end + +function module.new(this,lever,context) + this.motor = _G.GetInstance(lever,"Motor") + this.phase = _G.GetInstance(lever,"Phase") + this.phases = _G.GetInstance(lever,"Phases") + this.hold = _G.GetInstance(lever,"Hold") + this.click = _G.GetInstance(this.hold,"ClickDetector") + this.target = lever:GetAttribute("Target") or error("Needs target attribute.") + this.origin = lever:GetAttribute("Origin") or error("Needs target attribute.") + if _G.IsServer then + module.update(this,lever,context) + this.click.MouseClick:Connect(function() + if this.phase.Value == this.phases.Value - 1 then + this.phase.Value = 0 + else + this.phase.Value += 1 + end + print(this.phase.Value) + module.update(this,lever,context) + context.connected("ConnectorData",nil,lever.PrimaryPart,function(object) + print("firing",this.phase.Value) + context.message(object,nil,this.phase.Value/(this.phases.Value-1)) + end) + end) + end +end + +function module.update(this,lever,context) + if _G.IsServer then + position(this.motor,this.phase.Value,this.phases.Value,this.target,this.origin) + end +end + +module.manifest = { + "Lever" +} + +return module \ No newline at end of file diff --git a/scripts/modules/objects/Mesh.lua b/scripts/modules/objects/Mesh.lua new file mode 100644 index 0000000..4c16a3c --- /dev/null +++ b/scripts/modules/objects/Mesh.lua @@ -0,0 +1,26 @@ +local module = {} + +local function setup(this,mesh,context) + this.mesh.MeshId = string.format("rbxassetid://%i",this.meshid.Value) + this.mesh.TextureId = string.format("rbxassetid://%i",this.textureid.Value) + this.mesh.Scale = mesh.Size * this.scale.Value +end + +function module.new(this,mesh,context) + this.meshid = _G.GetInstance(mesh,"MeshId") + this.textureid = _G.GetInstance(mesh,"TextureId") + this.scale = _G.GetInstance(mesh,"Scale") + this.mesh = _G.GetInstance(mesh,"Mesh") + setup(this,mesh,context) + return this +end + +function module.update(this,mesh,context) + setup(this,mesh,context) +end + +module.manifest = { + "Prop" +} + +return module diff --git a/scripts/modules/objects/Piston.lua b/scripts/modules/objects/Piston.lua new file mode 100644 index 0000000..3c2b145 --- /dev/null +++ b/scripts/modules/objects/Piston.lua @@ -0,0 +1,40 @@ +local module = {} + +function module.new(this,piston,context) + this.servo = _G.GetInstance(piston,"Servo") + this.extension = _G.GetInstance(piston,"Extension") + this.binding = _G.GetInstance(piston,"Binding") + this.inverted = _G.GetInstance(piston,"Inverted") + this.force = _G.GetInstance(piston,"Force") + return this +end + +local begin = tick() +module.run = _G.Protect(function(this,servo,context) + local control = (this.control or context.controls[this.binding.Value] or 0) + if this.inverted.Value then control = 1 - control end + this.servo.TargetPosition = servo.Extension.Value * control * servo:GetScale() + (this.offset or 0) + context.replicate(this,servo,module,this.servo.TargetPosition) +end) + +module.replication_delta = 0.25 +function module.replicate(this,servo,context,value) + servo.ServoMaxForce = this.force.Value * servo:GetScale() ^ 2 + servo.TargetPosition = value +end + +module.messages = { + control = function(this,object,context,value) + this.control = value + end, + offset = function(this,object,context,value) + this.offset = value + end, +} +module.messages.default = module.messages.control + +module.manifest = { + "Piston" +} + +return module diff --git a/scripts/modules/objects/Propeller.lua b/scripts/modules/objects/Propeller.lua new file mode 100644 index 0000000..b01f0e3 --- /dev/null +++ b/scripts/modules/objects/Propeller.lua @@ -0,0 +1,126 @@ +local module = {} + +local dens = game.Workspace.AirDensity * -game.Workspace.CompensateDensity.Value +local bias = 0.2 + +local propeller_power_mod = 0.1 +local propeller_cut_mod = 5/2 +local propeller_reaction_mod = 0.05 + +game.Workspace.CompensateDensity.Changed:Connect(function() + dens = game.Workspace.AirDensity * -game.Workspace.CompensateDensity.Value +end) + +_G.Events.Command.Event:Connect(function(message) + local value = message:match("setprop%((.-)%)") + if not value then return end + if tonumber(value) then + game.Workspace.CompensateDensity = tonumber(value) + _G.Events.Notification:Fire("Set Prop Density","PropBias.Value = "..value) + else + _G.Events.Notification:Fire("Set Prop Density","Invalid number.") + end +end) + +local function setup(this,propeller,context) + local angle = _G.GetValue(propeller,"Trim",30) + for _,child in pairs(propeller:GetChildren()) do + if child.Name == "Blade" then + local welds = child:GetJoints() + local joint = child:FindFirstChild("Weld") + if not child:FindFirstChild("Weld") then + local c0 = propeller.PrimaryPart.CFrame:ToObjectSpace(child.CFrame) + joint = Instance.new("Motor6D") + joint.Name = "Weld" + joint.Parent = child + joint.Part0 = propeller.PrimaryPart + joint.Part1 = child + joint.C0 = c0 + end + local flip = 0 + if angle < 0 then flip = 180 end + local axis = (propeller:GetAttribute("Axis") or Vector3.new(0,0,1)) * (math.rad(-angle+flip)) + local rotation = CFrame.Angles(axis.X,axis.Y,axis.Z) + joint.C1 = rotation + for _,weld in pairs(welds) do if weld:IsA("WeldConstraint") then weld:Destroy() end end + end + end +end + +function module.new(this,propeller,context) + local size = propeller:GetExtentsSize() + this.scale = size.X * size.Z * 1.5 + this.length = size.X + this.power = _G.GetValue(propeller,"Force",1000) * this.scale * 0.01 + this.direction = _G.GetValue(propeller,"Direction",CFrame.identity) + this.rotor = propeller:FindFirstChild("Spin") or propeller:FindFirstChild("Hinge") + this.trim = _G.GetValue(propeller,"Trim",30) + this.blur = propeller:FindFirstChild("Blur") + if not this.rotor then error("Missing rotor") end + this.force = _G.GetInstance(propeller,"VectorForce") + this.sound = _G.GetInstance(this.rotor,"PropSound") + this.sound:Play() + setup(this,propeller,context) + return this +end + +function module.update(this,propeller,context) + print("upd") + setup(this,propeller,context) +end + +function module.idle(this,engine) return true end + +local a = 300 +local function limit(f) + return math.tanh(f/a)*a +end + +local begin = tick() +module.run = _G.Protect(function(this,propeller,context) + local angular_velocity = ((this.rotor.CFrame.Rotation * this.direction):PointToObjectSpace(this.rotor.AssemblyAngularVelocity)).X + local linear_velocity = angular_velocity * math.sin(math.rad(this.trim)) + local air_velocity = this.rotor.CFrame.Rotation:PointToObjectSpace(this.rotor.AssemblyLinearVelocity).X + local real_velocity = limit((linear_velocity * propeller_cut_mod * this.length) - air_velocity) + local rpm = (angular_velocity/(math.pi*2))*60 + local multiplier = (this.sound:GetAttribute("Multiplier") or 1) * 0.01 + this.force.Force = Vector3.new(0,0,-real_velocity*this.power*propeller_power_mod*dens) + this.rotor:ApplyAngularImpulse( + (this.rotor.CFrame.Rotation * this.direction).RightVector + * -angular_velocity + * math.cos(math.rad(this.trim)) + * propeller_reaction_mod) + context.replicate(this,propeller,module,math.abs(rpm*dens*multiplier)) + return true +end) + +module.replication_delta = 0.1 +function module.replicate(this,propeller,context,value) + this.sound.PlaybackSpeed = math.clamp(value,0.1,1.5) + this.sound.Volume = math.clamp(value,0,this.sound:GetAttribute("Max") or 1) + if this.blur then + this.blur.Transparency = math.clamp(1-value,0.3,1) + end + for _,blade in pairs(propeller:GetChildren()) do + if blade.Name == "Blade" then + blade.Transparency = math.clamp(value,0,0.7) + end + end +end + +module.messages = { + trim = function(this,object,context,value) + + end +} +module.messages.default = module.messages.trim + +module.manifest = { + "Propeller", + "Turbine", + "LargePropeller", + "BiPropeller", + "Jet" +} + +return module diff --git a/scripts/modules/objects/Rocket.lua b/scripts/modules/objects/Rocket.lua new file mode 100644 index 0000000..c7a6c25 --- /dev/null +++ b/scripts/modules/objects/Rocket.lua @@ -0,0 +1,46 @@ +local module = {} + +function module.new(this,rocket,context) + this.scale = rocket:GetExtentsSize().Magnitude -- same as with the equivalent statement on the wing! BRRRRRRRRRRR + this.binding = _G.GetInstance(rocket,"Binding") + this.exhaust = _G.GetInstance(rocket,"Exhaust") + this.sound = _G.GetInstance(this.exhaust,"Sound") + this.force = _G.GetInstance(rocket,"Force") + this.inverted = _G.GetInstance(rocket,"Inverted") + return this +end + +function module.idle(this,engine) return true end + +local begin = tick() +module.run = _G.Protect(function(this,engine,context) + local throttle = this.throttle or context.controls[_G.GetValue(engine,"Binding","throttle")] or this.Throttle or 0 + if this.inverted.Value then throttle = -throttle end + context.replicate(this,engine,module,math.clamp(throttle,0,1)) + return true +end) + +module.replication_delta = 0.25 +function module.replicate(this,rocket,context,throttle) + this.exhaust:ApplyImpulseAtPosition(this.exhaust.CFrame.RightVector * throttle * this.scale * this.force.Value * context.delta * -1000, this.exhaust.Position) + this.exhaust.Flame.Rate = throttle * 1000 + this.sound.Volume = math.clamp(throttle,0,1) + if _G.IsServer and this.sound.Volume > 0 then this.sound:Play() else this.sound:Pause() end +end + +module.messages = { + control = function(this,object,context,value) + this.throttle = value + end, + starter = function(this,object,context,value) + this.starter = value + end, +} +module.messages.default = module.messages.control + +module.manifest = { + "Rocket", + "BigRocket" +} + +return module diff --git a/scripts/modules/objects/Rope.lua b/scripts/modules/objects/Rope.lua new file mode 100644 index 0000000..8607bba --- /dev/null +++ b/scripts/modules/objects/Rope.lua @@ -0,0 +1,91 @@ +local module = {} + +local partStateModule = require(_G.Modules.PartStateModule) + +function setup(this,rope,context) + local function new(name,tab) + local part = Instance.new(name) + _G.Union(part,tab) + return part + end + + local node1 = rope["1"] + local node2 = rope["2"] + if rope.Name == "Shaft" then -- Make two universal constraints + local constraint1 = new("UniversalConstraint",{ + Parent = node1, + Attachment0 = new("Attachment",{ + Parent = node1 + }), + Attachment1 = new("Attachment",{ + Parent = rope, + WorldCFrame = node1.CFrame + }) + }) + local constraint2 = new("UniversalConstraint",{ + Parent = node2, + Attachment0 = new("Attachment",{ + Parent = node2 + }), + Attachment1 = new("Attachment",{ + Parent = rope, + WorldCFrame = node2.CFrame + }) + }) + elseif rope.Name == "Rope" or rope.Name == "Rod2" then + rope.Transparency = 1 + rope.CanCollide = false + rope.Massless = true + + local name + if rope.name == "Rope" then name = "RopeConstraint" end + if rope.Name == "Rod2" then name = "RodConstraint" end + local constraint = new(name,{ + Parent = rope, + Attachment0 = new("Attachment",{ + Parent = node1 + }), + Attachment1 = new("Attachment",{ + Parent = node2 + }), + Visible = true, + Thickness = rope.Size.Y + }) + if rope.Length.Value == 0 then rope.Length.Value = constraint.CurrentDistance end + constraint.Length = rope.Length.Value + end + + node1.CanCollide = true -- Resize and make nodes non-collide + node2.CanCollide = true + node1.Size = Vector3.one * rope.Size.Y + node2.Size = node1.Size + for index,joint in pairs(rope:GetJoints()) do -- Fix welds in new version + if joint:IsA("WeldConstraint") then + local other = joint.Part0 + if joint.Part0 == rope then other = joint.Part1 end + local closest = node1 + if (node2.Position-other.Position).Magnitude < (node1.Position-other.Position).Magnitude then closest = node2 end + new("WeldConstraint",{ + Parent = rope, + Part0 = closest, + Part1 = other, + Name = "BuiltWeldConstraint" + }) + if index > 1 then joint:Destroy() end + end + end +end + + +function module.new(this,rope,context) + if _G.IsServer then setup(this,rope,context) end + return this +end + +module.manifest = { + "Rod2", + "Rope", + "Shaft" +} + +return module diff --git a/scripts/modules/objects/Servo.lua b/scripts/modules/objects/Servo.lua new file mode 100644 index 0000000..1c714d0 --- /dev/null +++ b/scripts/modules/objects/Servo.lua @@ -0,0 +1,99 @@ +local module = {} + +local function targetHinge(target,hinge,stationary,angle,negative) + local attachment = stationary:FindFirstChildWhichIsA("Attachment") + + local o = attachment.WorldCFrame:PointToObjectSpace(target) + + local x,y,z = CFrame.lookAlong(attachment.WorldCFrame.Position,attachment.WorldCFrame.UpVector,attachment.WorldCFrame.RightVector):ToObjectSpace( + CFrame.lookAt( + attachment.WorldCFrame.Position, + attachment.WorldCFrame:PointToWorldSpace(Vector3.new(0,o.Y,o.Z)*negative) + )):ToEulerAnglesYXZ() + hinge.TargetAngle = -math.deg(y) + angle +end + +function module.new(this,servo,context) + this.scale = servo:GetExtentsSize().Magnitude + this.hinge = _G.GetInstance(servo,"HingeConstraint") + this.binding = _G.GetInstance(servo,"Binding") + this.inverted = servo:FindFirstChild("Inverted") + this.offset = servo:FindFirstChild("Offset") + this.range = servo:FindFirstChild("Angle") + this.target = servo:FindFirstChild("Target") + this.torque = servo:FindFirstChild("Torque") + return this +end + +local begin = tick() +module.run = _G.Protect(function(this,servo,context) + local shouldInvert = 1 + local shouldOffset = 0 + local pitchTrim = 0 + local angle = 30 + if context.seat and context.seat:FindFirstChild("PitchTrim") then pitchTrim = context.seat.PitchTrim.Value end + if this.inverted and this.inverted.Value then shouldInvert = -1 end + if this.offset and this.offset.Value then shouldOffset = this.offset.Value + (this.starter or 0) end + if this.range and this.range.Value then angle = this.range.Value end + if this.torque and this.torque.Value then this.hinge.MotorMaxTorque = this.torque.Value * ((servo:GetAttribute("Health") or 1) / (servo:GetAttribute("MaxHealth") or 1)) end + local control = (this.control or context.controls[this.binding.Value]) + if this.target and this.target.Value then + this.hinge.LimitsEnabled = false + if context.cursor then + targetHinge(context.cursor,this.hinge,this.hinge.Attachment1.Parent,this.offset.Value,shouldInvert) + end + elseif this.binding.Value == "auto" and context.seat then + local pos = context.seat:GetPivot():ToObjectSpace(this.hinge.Attachment1.WorldCFrame) + local dir = pos.RightVector + pos = pos.Position.Unit + this.hinge.LimitsEnabled = false + this.hinge.TargetAngle = ( ( + -context.controls.pitch*dir.x*pos.Z + -context.controls.yaw*dir.y*pos.Z + +context.controls.roll*dir.x*pos.X + ) * angle + pitchTrim*dir.x*pos.Z + ) * shouldInvert + shouldOffset + elseif control then + if this.binding.Value == "pitch" then shouldOffset = shouldOffset + pitchTrim end + this.hinge.LimitsEnabled = true + this.hinge.TargetAngle = control * angle * shouldInvert + shouldOffset + end + local distance = (this.hinge.Attachment1.WorldCFrame.Position - this.hinge.Attachment1.WorldCFrame.Position).Magnitude + if distance < 2 then + context.replicate(this,servo,module,this.hinge.TargetAngle,this.hinge.LimitsEnabled) + return true + else + context.replicate(this,servo,module,nil,nil) + return false + end +end) + +module.replication_delta = 0.25 +function module.replicate(this,servo,context,angle) + if not angle then + this.hinge:Destroy() + else + this.hinge.TargetAngle = angle + end +end + +module.messages = { + control = function(this,object,context,value) + this.control = value + end, + offset = function(this,object,context,value) + this.starter = value + end, +} +module.messages.default = module.messages.control + +module.manifest = { + "FlatServoRoll", + "FlatServoPitch", + "Servo", + "ControlSheet", + "NanoControlSheet", + "MetalServo" +} + +return module diff --git a/scripts/modules/objects/Speaker.lua b/scripts/modules/objects/Speaker.lua new file mode 100644 index 0000000..8a487da --- /dev/null +++ b/scripts/modules/objects/Speaker.lua @@ -0,0 +1,26 @@ +local module = {} + +function module.new(this,speaker,context) + this.main = _G.GetInstance(speaker,"Main") + this.tts = _G.GetInstance(this.main,"AudioTextToSpeech") +end + +function module.replicate(this,speaker,context,value) + this.tts.Text = tostring(value or "") or "" + if _G.IsClient then + if this.tts.IsPlaying then return end + this.tts:Play() + end +end + +module.messages = { + default = function(this,speaker,context,value) + context.replicate(this,speaker,module,value) + end +} + +module.manifest = { + "Speaker" +} + +return module \ No newline at end of file diff --git a/scripts/modules/objects/Spring.lua b/scripts/modules/objects/Spring.lua new file mode 100644 index 0000000..f8bd78f --- /dev/null +++ b/scripts/modules/objects/Spring.lua @@ -0,0 +1,27 @@ +local module = {} + +local spring_modifier = 1 +local function setup(this,spring,context) + this.spring.Damping = _G.GetValue(spring,"Dampness",this.spring.Damping) + this.spring.Stiffness = _G.GetValue(spring,"Stiffness",this.spring.Stiffness) * spring_modifier * spring:GetScale()^3 + this.spring.Visible = this.spring.Stiffness > 0 +end + +function module.new(this,spring,context) + this.spring = _G.GetInstance(spring,"SpringConstraint") + setup(this,spring,context) +end + +function module.update(this,spring,context) + print("setup") + setup(this,spring,context) +end + +module.update = module.new + +module.manifest = { + "Coilover", + "Wheel" +} + +return module \ No newline at end of file diff --git a/scripts/modules/objects/Turbine.lua b/scripts/modules/objects/Turbine.lua new file mode 100644 index 0000000..9a1ca1d --- /dev/null +++ b/scripts/modules/objects/Turbine.lua @@ -0,0 +1,38 @@ +local module = {} + +local cool_rate = 0.5 +function module.new(this,turbine,context) + this.heat = 0 + this.hinge = _G.GetInstance(turbine,"HingeConstraint") + this.speed = _G.GetInstance(turbine,"Speed") + this.inverted = _G.GetInstance(turbine,"Inverted") +end + +function module.run(this,turbine,context) + this.heat = this.heat * (1 - cool_rate * context.delta) -- wrong but idc + local mul = 1 + if this.inverted.Value then mul = -1 end + this.hinge.AngularVelocity = (1-(1/(1 + this.heat))) * this.speed.Value * mul + this.hinge.MotorMaxTorque = 10000 * this.speed.Value + context.replicate(this,turbine,module,this.heat) + return true +end + +function module.replicate(this,turbine,context,heat) + this.heat = heat +end + +module.update = module.new + +module.messages = { + flux = function(this,turbine,context,heat) + this.heat = this.heat + heat + end, +} +module.messages.default = nil + +module.manifest = { + "Turbine1" +} + +return module \ No newline at end of file diff --git a/scripts/server/AutoModule b/scripts/server/AutoModule new file mode 100644 index 0000000..b5f3154 --- /dev/null +++ b/scripts/server/AutoModule @@ -0,0 +1,205 @@ +chat = {} +local rchat=game:GetService("Chat") +function chat.Chat(ignore,object,text) + if not object then return end + rchat:Chat(object,text) + --print("AUTO:",text) -- +end + +local function advancedTarget(pS,t,o) -- pS = projectileSpeed, t = targetPart, o = originPart + -- So, I understand the maths in this. I also understand that it forms a circle for radius of targetting, also the curve to find it is quadratic and there is sine somewhere too + -- https://devforum.roblox.com/t/making-a-lead-shot-indicator/325666/9 (figure out the maths here when you're older, i understand it but not confidently) + local tV = t.Velocity * 1.1 -- target Velocity, *1.5 because of compensation + local di = t.Position - o.Position -- displacement, origin to target + local a = pS^2 - tV:Dot(tV) -- a,b,c as in quadratic equation so that ax^2 + bx + c = 0 + local b = -2 * tV:Dot(di) + local c = -di:Dot(di) + local d = b^2 - 4*a*c -- d as an intermediate value for quadratic formula + if d < 0 then return t.Position end -- failed, let's just hope we hit it anyways by luck + local t0 = (-b + math.sqrt(d)) / (2*a) -- hello mr. hegarty, i exist + local t1 = (-b - math.sqrt(d)) / (2*a) + local ti = 0 + if t0 > 0 then ti = t0 end + if t1 > 0 then ti = t1 end + local target = t.CFrame.Position + tV * ti + return target +end + +function travelToLocation(a) + a.humanoid:MoveTo(a.target.Position) +end + +function findPlayer(a) + local closest + local distance = 100 + for _,player in pairs(game.Players:GetPlayers()) do + if player.Character and player.Character:FindFirstChild("Humanoid") then + local currentDistance = (player.Character.PrimaryPart.Position - a.head.Position).Magnitude + if currentDistance < distance then distance = currentDistance end + closest = player.Character.PrimaryPart + end + end + if closest then a.task = travelToLocation a.target = closest end +end + +function enterPlane(a) + --[[local pathfinding = game:GetService("PathfindingService") + local path = pathfinding:CreatePath({ + AgentCanJump = true, + AgentHeight = 4, + AgentWidth = 4 + }) + chat:Chat(a.head,"computing") + path:ComputeAsync(a.model.PrimaryPart.Position,a.plane.Position) + if path.Status ~= Enum.PathStatus.Success then + chat:Chat(a.head,"no path") + a.model:SetPrimaryPartCFrame(a.plane.CFrame) + a.task = getTask return end + + a.plane.Material = Enum.Material.Neon + a.plane.Transparency = 0.5 + + chat:Chat(a.head,"walking") + local waypointIndex = 0 + local waypoints = path:GetWaypoints() + local function operateWaypoint() + if not a.operate then return end + if a.humanoid.SeatPart then chat:Chat(a.head,"piloting") a.task = flyPlane return end + waypointIndex = waypointIndex + 1 + if waypointIndex > #waypoints or not waypoints[waypointIndex] then return end + a.humanoid:MoveTo(waypoints[waypointIndex].Position) + + local shouldReturn = false + local event = a.humanoid.MoveToFinished:Connect(function() + if waypoints[waypointIndex] and not a.humanoid.SeatPart and waypoints[waypointIndex].Action == Enum.PathWaypointAction.Jump then + a.humanoid.Jump = true + end + shouldReturn = true + operateWaypoint() + end) + wait(1) + if not a.plane or not a.plane:FindFirstChild("Pilot") then return end + event:Disconnect() + if (a.model.PrimaryPart.Position - a.plane.Position).Magnitude < 6 then + + end + if shouldReturn then return end + operateWaypoint() + end + operateWaypoint()]] + a.model:SetPrimaryPartCFrame(a.plane.CFrame) + a.task = flyPlaneStraight + a.v.parent = a.plane.Parent.Parent + a.target = game.Workspace.BotTarget.Position +end +local flight = require(_G.Modules.ObjectInteraction.Main) +function flyPlaneStraight(a) + if not a.flyPlaneStraightTick then a.flyPlaneStraightTick = tick() end + if (tick() - a.flyPlaneStraightTick) > 3 then a.task = flyPlane a.flyPlaneStraightTick = nil end + a.v.bindings.throttle = 0.8 + a.v.bindings.pitch = 0.3 + a.v.bindings.roll = 0 + a.v.bindings.yaw = 0 + local tar = a.plane.CFrame.LookVector + tar = Vector3.new(tar.X,0,tar.Z).Unit + local pos = (a.plane.Position + tar * 1000 + Vector3.new(0,150,0)) + a.v:aimTowards(pos) +end +function stop(a) +end +local trajectoryPart = Instance.new("Part") +for index,item in pairs({ + Color = Color3.new(1,1,1), + Material = Enum.Material.Neon, + Transparency = 0.2, + Size = Vector3.new(6,6,6), + CanCollide = false, + Anchored = true, + Shape = Enum.PartType.Ball, + Parent = game.Workspace + }) do + trajectoryPart[index] = item +end +local targetPart = trajectoryPart:Clone() +targetPart.Parent = game.Workspace +function flyPlane(a) + a.target = a.model.Target.Value.PrimaryPart.Position + local trajectory = a.plane.Velocity*2 + a.plane.Position + trajectoryPart.Position = trajectory + targetPart.Position = a.target + print(trajectory) + if trajectory.Y < -20 then + a.task = flyPlaneStraight + print("recover!") + end + a.v.bindings.pitch = 0.0 + a.v.bindings.yaw = 0 + a.v:aimTowards(a.target) + local speed = a.plane.Velocity.Magnitude + if speed > 350 or speed < 50 or a.plane.Position.Y < -40 then print("stop!",speed) a.task = stop a.v.bindings.throttle = 0 task.delay(1,function() a.humanoid.Health = 0 end) end +end +function getTask(a) + chat:Chat(a.head,"task") + wait(0.5) + if a.humanoid.SeatPart then chat:Chat(a.head,"piloting") a.task = flyPlaneStraight return end + a.task = locatePlane + chat:Chat(a.head,"pilot") +end +function selfDestruct(a) + chat:Chat(a.head,"destruct") + a.humanoid.Health = 0 +end +function locatePlane(a) + chat:Chat(a.head,"find plane") + a.plane = nil + local distance = 100 + for _,object in pairs(game.Workspace.Auto:GetChildren()) do + local seat = object:FindFirstChildWhichIsA("VehicleSeat",true) + if seat and not seat.Occupant then + local offset = (seat.Position - a.head.Position).Magnitude + if offset < distance then + distance = offset + a.plane = seat + end + end + end + if a.plane then + chat:Chat(a.head,"found plane") + a.task = enterPlane + return + end + chat:Chat(a.head,"no plane") + a.task = selfDestruct +end +function hibernate(a) + +end +function init(auto) + local a = {} + a.model = auto + a.head = auto.Head + if a.head:FindFirstChildWhichIsA("SurfaceGui") then a.head.SurfaceGui:Destroy() end + a.face = a.head.Face + a.humanoid = auto.Humanoid + a.team = auto.Team.Value + a.operate = true + wait() + a.plane = a.humanoid.SeatPart + a.task = flyPlaneStraight + a.v = flight.initialise(a.head) + a.v.parent = a.humanoid.SeatPart.Parent.Parent + a.v:seatEntered(true,a.humanoid.SeatPart) + a.target = (a.plane.CFrame * CFrame.new(0,0,1000)).Position + a.target = Vector3.new(a.target.X,a.plane.CFrame.Position.Y + 100,a.target.Z) + a.humanoid.Died:Connect(function() + wait(2) + a.operate = false + a.v.operate = false + a.v:destroy() + a.model:Destroy() + end) + while wait((1/30)*3) and a.operate do + a.task(a) + end +end +return getfenv() \ No newline at end of file diff --git a/scripts/server/AutoSpawnModule b/scripts/server/AutoSpawnModule new file mode 100644 index 0000000..b1f8c55 --- /dev/null +++ b/scripts/server/AutoSpawnModule @@ -0,0 +1,63 @@ +local autoModel = _G.Storage.Assets.Autos:WaitForChild("Auto") +local planeModel = _G.Storage.Assets.Vehicles:WaitForChild("BP-10") +local purchaseModule = require(game.ServerScriptService.PurchaseModule) +teams = {} +function init(parent) + wait(1) + local m = parent + local team = game.Teams[parent.Parent.Name] + local autoSpawn = parent:WaitForChild("AutoSpawn") + local planeSpawn = parent:WaitForChild("PlaneSpawn") + + local function lockPlane(plane) + plane.Pilot.Anchored = true + plane.Pilot.Changed:Connect(function(name) + if name == "Occupant" then + if plane.Pilot.Occupant then wait(3) plane.Pilot.Anchored = false end + end + end) + end + + if not teams[team.Name] then teams[team.Name] = 0 end + + local function spawnAuto() + local property = game.Workspace.Property[team.Name] + local newAuto = autoModel:Clone() + newAuto.Parent = property + newAuto.PrimaryPart.CFrame = autoSpawn.CFrame + Vector3.new(0,2,0) + newAuto["Body Colors"].TorsoColor = team.TeamColor + + local planeModel = planeModel:Clone() + planeModel.Parent = property + planeModel:SetPrimaryPartCFrame(planeSpawn.CFrame) + --lockPlane(planeModel) + + purchaseModule.colorVehicle(planeModel,team) + purchaseModule.collisionCheck(planeModel,0.5,10) + + newAuto.Humanoid.Died:Connect(function() + wait(3) + planeModel:Destroy() + newAuto:Destroy() + teams[team.Name]-=1 + end) + + teams[team.Name] += 1 + + end + + while parent.Parent do + if not (teams[team.Name] < _G.Storage.Variables.AutoNumber.Value) then wait(30) continue end + teams[team.Name] = 0 + for _,object in pairs(game.Workspace.Property[team.Name]:GetChildren()) do + if object.Name == "Auto" and object.PrimaryPart and object.PrimaryPart.Velocity.Magnitude > 30 then -- and object.PrimaryPart.Velocity.Magnitude > 50 then + teams[team.Name]+=1 + end + end + _G.AutoTeams = teams + --print("Auto: ",team.Name,teams[team.Name]) + spawnAuto() + wait(30) + end +end +return getfenv() \ No newline at end of file diff --git a/scripts/server/GenerationForeman b/scripts/server/GenerationForeman new file mode 100644 index 0000000..12c4f73 --- /dev/null +++ b/scripts/server/GenerationForeman @@ -0,0 +1,123 @@ +require(game.ReplicatedFirst.ReadyModule)("GenerationForeman") +local rules = require(_G.Modules.RuleModule) + +local CHUNK_SIZE = 512 -- wae +local RADIUS = 3 +--if _G.IsStudio then RADIUS = 1 end -- stop my computer from dying +local LEEWAY = 20 -- WHY IS THIS SO BIG WTF +local COMBAT_RADIUS = 6 +local POOL_SIZE = 4 -- + +local CHUNK_OFFSET_X = -1150 +local CHUNK_OFFSET_Z = 120 + +local pool = 0 +local chunks = {} +local schunks = {} +local _c = {} +_c.__index = _c +_c.delete = function(this) + schunks[this.index.x][this.index.z] = nil + --if #schunks[index.x] == 0 then schunks[index.x] = nil end + if this.generator then this.generator:SendMessage("delete") end + chunks[this.index] = nil +end +local function chunk(x,z) + if not schunks[x] then schunks[x] = {} end + local i = {x=x,z=z} + local first,second + local c + + c = {viable = true, permanent = nil, generator = nil, started = false, index = i} + setmetatable(c,_c) + chunks[i]=c + schunks[x][z]=c + + spawn(function() + repeat wait() until pool < POOL_SIZE + if not c.viable then return end + pool = pool + 1 + c.generator = _G.Modules.Generator:Clone() + c.generator.Parent = game.Workspace + c.generator:SendMessage("generate",x * CHUNK_SIZE,z * CHUNK_SIZE,CHUNK_OFFSET_X,CHUNK_OFFSET_Z) + c.generator.Changed:Wait() + pool = pool - 1 + end) + + return c +end + +local function generateCombatZone() + _G.Remotes.Notification:FireAllClients("Generation","Generating combat zone, please wait.") + --[[rules.setRule("GenerationProcessing",true) + for x = -COMBAT_RADIUS,COMBAT_RADIUS do + for z = -COMBAT_RADIUS*2,COMBAT_RADIUS*2 do + chunk(x,z).permanent = true + end + end + wait() + repeat wait() until pool == 0 + rules.setRule("GenerationProcessing",false)]] + _G.Remotes.Notification:FireAllClients("Generation","Generation complete.") +end + +local function generateForPlayer() + while wait(0.5) and rules.rules.GenerationFollowPlayer do + for index,chunk in pairs(chunks) do + chunk.viable = false + end + local perPlayer = function(p) + for index,chunk in pairs(chunks) do + if Vector2.new(p.x-index.x*CHUNK_SIZE,p.z-index.z*CHUNK_SIZE).Magnitude < CHUNK_SIZE*(RADIUS+LEEWAY) then + chunk.viable = true + end + end + for x=math.ceil(p.x/CHUNK_SIZE)-RADIUS,math.ceil(p.x/CHUNK_SIZE)+RADIUS do + for z=math.ceil(p.z/CHUNK_SIZE)-RADIUS,math.ceil(p.z/CHUNK_SIZE)+RADIUS do + if not schunks[x] then schunks[x] = {} end + if not schunks[x][z] then + chunk(x,z) + end + end + end + end + + for _,player in pairs(game.Players:GetPlayers()) do + if player.Character then + if player.Character.PrimaryPart then + perPlayer(player.Character.PrimaryPart.Position) + end + end + end + + perPlayer(game.Workspace.CurrentCamera.CFrame.Position) + + for index,chunk in pairs(chunks) do + if not chunk.viable and not chunk.permanent then + chunk:delete() + end + end + end +end + +local function reloadGeneration() + for index,chunk in pairs(chunks) do + chunk:delete() + end +end + +rules.listenRule("GenerationNoiseOffset",function(x,z) + CHUNK_OFFSET_X = x + CHUNK_OFFSET_Z = z + reloadGeneration() +end) +rules.listenRule("GenerationFollowPlayer",function(yes) + if yes then generateForPlayer() else reloadGeneration() end +end) +local r = Random.new(os.time()) +rules.listenRule("GenerationCombatZone",function() + CHUNK_OFFSET_X = r:NextInteger(-10000,10000) + CHUNK_OFFSET_Z = r:NextInteger(-10000,10000) + reloadGeneration() + generateCombatZone() +end) \ No newline at end of file diff --git a/scripts/server/GenerationInsurance b/scripts/server/GenerationInsurance new file mode 100644 index 0000000..14a3771 --- /dev/null +++ b/scripts/server/GenerationInsurance @@ -0,0 +1,2 @@ +require(game.ReplicatedFirst.ReadyModule)("GenerationInsurance") +if not _G.IsStudio then script.Parent.GenerationForeman.Enabled = true end \ No newline at end of file diff --git a/scripts/server/ServerScriptNew b/scripts/server/ServerScriptNew new file mode 100644 index 0000000..a2d8031 --- /dev/null +++ b/scripts/server/ServerScriptNew @@ -0,0 +1,793 @@ +require(game.ReplicatedFirst.ReadyModule)("ServerScript") + +local objectModule = require(_G.Modules.ObjectModule) +--local objectInteraction = require(_G.Modules.ObjectInteraction.Main) +local rules = require(_G.Modules.RuleModule) +local commands = require(_G.Modules.CommandsModule) +local partStateModule = require(_G.Modules.PartStateModule) + +DEFAULT_MAX_SAVES = 20 +SAVE_DATA_LIMIT = 5*10^6 +PLAYER_INTERACTION_DISTANCE = 100 +PLAYER_MAX_FLOOD = 10 +PLAYER_FLOOD_TICK = 4 +VEHICLE_SAVE_STORE = "VehicleSaveStore" +VEHICLE_SAVE_INFO_STORE = "VehicleSaveInfo" +USER_MOD_STORE = "ModStore" +MAIN_GAME_PLACEID = 11370438752 +PLACE_COOLDOWN = 0.75 + +--[[if game.PlaceId ~= MAIN_GAME_PLACEID then + VEHICLE_SAVE_STORE = "VehicleSaveStoreTest" + VEHICLE_SAVE_INFO_STORE = "VehicleSaveInfoTest" + USER_MOD_STORE = "ModStoreTest" +end]] + +for index,value in pairs(_G.Rules[_G.RuleMode]) do + rules.setRule(index,value) +end + +local function GetStoreData(store,key,ver) + local success,value = pcall(function() -- Protection call + local data + if ver then -- When a version is provided get it based on that + data = store:GetVersionAsync(key,ver) + else + data = store:GetAsync(key) + end + return data + end) + if ver then print(store,key,ver) end + if not success then warn(value) end + return success,value +end + +local function SetStoreData(store,key,val) + local success = pcall(function() + store:SetAsync(key,val) + end) + return success +end + +PartExperience = {} +PartCost = {} + +local saves = {} + +local players = {} + +local admins = { + [558665455] = true +} + +local _player = {} -- Metatable for player class +_player.__index = _player + +_player.Remove = function(this) -- Clear data + players[this.userid] = nil + players[this.instance] = nil +end + +_player.IsAdmin = function(this) -- Has commands permissions + if admins[this.instance.UserId] then return true end + if this.admin then return true end +end + +_player.DrainFloodGate = function(this,amount) + this.flood = this.flood - amount + if this.flood < 0 then this.flood = 0 end +end + +_player.CheckLastPlaced = function(this) + local possible = ((tick() - this.last_placed) > PLACE_COOLDOWN) + if possible then this.last_placed = tick() end + return possible +end + +_player.CheckFloodGate = function(this,amount) + this.flood = this.flood + amount + if this.flood > PLAYER_MAX_FLOOD then return false else return true end +end + +_player.OwnsPart = function(this,part) + return _G.Methods.playerOwnsPart(this.instance,part) +end + +_player.OwnsSave = function(this,group) -- Owns a save DataStore if... + if this:IsAdmin() or group:match("public") or group == this.userid then + return true + else + this:Notify("Permission Error","You do not own this save: "..group) + warn("Permission error: "..group.." "..this.userid) + return false + end +end + +_player.InRange = function(this,cframe) -- Is the player close enough to interact + this = this.instance + if + not this or + not cframe or + not this.Character or + not this.Character.PrimaryPart + then + print("Bad parameters",this,cframe,this.Character) + end + return (cframe-this.Character.PrimaryPart.Position).Position.Magnitude < PLAYER_INTERACTION_DISTANCE +end + +_player.CanAfford = function(this,part) +end + +_player.CanUsePart = function(this,part) + local experience = PartExperience[part] + if experience and experience > this.stats.experience then return false end + local cost = PartCost[part] + if cost and cost > this.stats.money then return false end + return not not part:IsDescendantOf(_G.Storage.Parts) +end + +_player.Notify = function(this,title,message) + if this.dummy then return end + _G.Remotes.Notification:FireClient(this.instance,title,message) +end + +_player.GetParts = function(this) + return _G.Tags:GetTagged("P_"..this.userid) +end + +_player.WaitForStatistics = function(this) + repeat + wait() + until this.stats +end + +_player.EvaluateSaves = function(this) + this:WaitForStatistics() + if this.evaluate_save_lock then repeat wait() until not this.evaluate_save_lock end + this.evaluate_save_lock = true + this.max_saves = DEFAULT_MAX_SAVES + (this.stats.additional_saves or 0) + if game:GetService("MarketplaceService"):UserOwnsGamePassAsync(tonumber(this.userid),1014956678) then + this.max_saves = this.max_saves + 20 + end + this.evaluate_save_lock = false +end + +_player.LoadSave = function(this,group,key) + if not this:CheckFloodGate(1) then return end + + --print(string.format("pg:%s,pk:%s",group,key)) + + local ver + local _group,_key = group:match("(.-):(.+)") + if _group and _key then ver = key key = _key group = _group end + + if not players[group] then return end + if not players[group].saves then error(group,key) end + + if not this:OwnsSave(group) then return end + + --print(string.format("k:%s,g:%s,v:%s",key,group,ver or "nil")) + if players[group].saves[key..(ver or "")] then + return players[group].saves[key] + else + local success,data = GetStoreData(players[group].stores.saves,key,ver) + if success then + if not ver then players[group].saves[key] = data end + return data + end + end +end + +players = { + public = setmetatable({ + parttag = "P_ublic", + userid = "public", + flood = -math.huge, + saves = {}, + stores = { + saves = _G.Data:GetDataStore(VEHICLE_SAVE_STORE,"public"), + save_info = _G.Data:GetDataStore(VEHICLE_SAVE_INFO_STORE,"public") + } + },_player) +} + +local function GetPlayer(id) -- Get a player by their id AS A STRING + local player = players[id] or players[tostring(id)] + if not player then error("No player data identified by "..tostring(id),2) end + return player +end + +local function AddPlayer(instance) + local data = { -- The player data... + instance = instance, -- Player instance in game.Players + userid = tostring(instance.UserId), + dummy = not instance.Parent, + name = instance.DisplayName, + saves = {}, -- Vehicle{} [] + flood = 0, -- If this gets too high, block remotes + stats = nil, + join = tick(), + last_placed = tick(), + max_saves = DEFAULT_MAX_SAVES, + parttag = "P_"..tostring(instance.UserId), + stores = {} -- DataStore + } + setmetatable(data,_player) -- ...made into a class + players[instance] = data -- Register them by id and instance + players[data.userid] = data + data.stores.saves = _G.Data:GetDataStore(VEHICLE_SAVE_STORE,data.userid) -- Vehicles + data.stores.mods = _G.Data:GetDataStore(USER_MOD_STORE,data.userid) -- Properties + data.stores.save_info = _G.Data:GetDataStore(VEHICLE_SAVE_INFO_STORE,data.userid) -- Vehicle Save Info + _,data.stats = GetStoreData(data.stores.mods,"stats") + if data.stats then + data.stats = _G.Http:JSONDecode(data.stats) + else + data.stats = {} + end + _G.Default(data.stats,{ + experience = 0, + money = 100, + extra_saves = 0, + playtime = 0, + color = require(_G.Modules.NameColorModule).ComputeNameColor(instance.Name) + }) + wait(2) + local playtime = data.stats.playtime + if playtime ~= 0 then + local ending = "seconds" + if playtime > 60 then ending = "minutes" playtime = playtime / 60 + if playtime > 60 then ending = "hours" playtime = playtime / 60 + if playtime > 24 then ending = "days" playtime = playtime / 24 + end + end + end + data:Notify("Welcome",string.format("Welcome back to Plane Building! You've been playing for %d %s!",math.floor(playtime),ending)) + if game.PlaceId ~= MAIN_GAME_PLACEID then + data:Notify("Warning",string.format("You are not in the main game place! Irreversible save corruption and gameplay bugs are likely!")) + end + else + data:Notify("Welcome","Hey newbie, have a warm welcome to Plane Building! Thanks so much for playing!") + end +end +game.Players.PlayerAdded:Connect(AddPlayer) + +local function RemovePlayer(instance) -- For the event + local player = GetPlayer(instance) + player.stats.playtime = player.stats.playtime + (tick() - player.join) + SetStoreData(player.stores.mods,"stats",_G.Http:JSONEncode(player.stats)) + player:Remove() +end +game.Players.PlayerRemoving:Connect(RemovePlayer) + +local function Weld(part1,part2,temporary) -- Make a BUILDING weld + local weld = Instance.new("WeldConstraint") + weld.Name = "BuiltWeldConstraint" + weld.Parent = part1 + weld.Part0 = part1 + weld.Part1 = part2 + if temporary then weld.Name = "WeldConstraint" end -- Or not. + return weld +end + +local function SetupPart(player,part) + part = part:Clone() + part.Parent = game.Workspace.Objects + _G.Tags:AddTag(part,"P_"..player.userid) + part:SetAttribute("NetworkOwner",player.userid) + return part +end + +local function CopyTags(part1,part2) + for _,tag in pairs(_G.Tags:GetTags(part2)) do + if not part1:HasTag(tag) and string.sub(tag,1,2) == "P_" then + part1:AddTag(tag) + end + end +end + +local function AttemptWeld(player,part,connection) + local new_weld + if connection and + (player:OwnsPart(connection) or player:OwnsPart(connection.Parent)) + then + local attach = part + if part:IsA("Model") then attach = attach.PrimaryPart end + new_weld = Weld(attach,connection) + new_weld.Parent = part + end + CopyTags(part,connection) + return new_weld +end + +local function ResizePart(part,size) + if part:IsA("Model") then + part:ScaleTo(size.Magnitude) + else + part.Size = size + end + part:SetAttribute("Size",size) +end + +local function _CreatePart(player,part,cframe,connection,scale) + local player = GetPlayer(player) + player:CheckFloodGate(4) + if + not player:CheckLastPlaced() or + not player:CanUsePart(part) + then + return + end + if connection then cframe = connection.CFrame:ToWorldSpace(cframe) end + if not player:InRange(cframe) then return end + part = part:Clone() + part.Parent = game.Workspace.Objects + if scale then ResizePart(part,scale) end + _G.Methods.setItemCFrame(part,cframe) + AttemptWeld(player,part,connection) + --[[if part:IsA("Model") then + _G.Methods.setNetworkOwner(part.PrimaryPart,player.instance) + else + _G.Methods.setNetworkOwner(part,player.instance) + end]] + part:AddTag(player.parttag) + wait() + return part +end + +local function ConfigurePart(player,part,own) + part:AddTag(player.parttag) + part.Parent = game.Workspace.Objects + local owner + if own then owner = player.instance end + objectModule.add(part,owner) + part:SetAttribute("Owner",player.instance.UserId) + --local modules = objectInteraction.modulesByName[part.Name] + --if modules then for _,module in pairs(modules) do if module.setup then module.setup(part) end end end +end + +local function CreatePart(player,id,...) + local player = GetPlayer(player) + player:CheckFloodGate(4) + local template = partStateModule.get_part_by_id(id) + if not template then error(string.format("No part for id: "..tostring(id))) return end + if + not player:CheckLastPlaced() or + not player:CanUsePart(template) + then + return + end + local state = partStateModule.new() + state.owner = player.instance + state:deserialise(id,...) + local part = state:create() + ConfigurePart(player,part,true) + wait() + return part +end +_G.Remotes.Create.OnServerInvoke = CreatePart + +local function _CreatePoly(player,part,nodes,connections) + local player = GetPlayer(player) + if + player:CheckFloodGate(6) and + player:CheckLastPlaced() and + player:CanUsePart(part) + then + for index,node in pairs(nodes) do + if node.connection then node.cframe = node.connection.CFrame:ToWorldSpace(node.cframe) end + end + for index,node in pairs(nodes) do + if (nodes[1].cframe.Position - node.cframe.Position).Magnitude > 80 then return end + if not player:InRange(node.cframe) then return end + end + part = part:Clone() + part.Parent = game.Workspace.Objects + part:AddTag(player.parttag) + if #nodes == 2 then + local a = nodes[1].cframe + local b = nodes[2].cframe + local d = (a-b).Magnitude + part.CFrame = CFrame.lookAt(a.Position,b.Position,a.UpVector) * CFrame.new(d * 0.5,0,0) + elseif #nodes == 3 then + _G.Methods.form_triangle(part.A,part.B,nodes[1].cframe.Position,nodes[2].cframe.Position,nodes[3].cframe.Position) + end + Weld(part.A,part.B) + for _,connection in pairs(connections) do + AttemptWeld(player,part.PrimaryPart,connection) + end + return part + end +end +--_G.Remotes.Poly.OnServerInvoke = CreatePoly + +local function TweakEffect(player,effect,rate,enabled) + effect.Enabled = enabled + effect.Rate = rate +end +_G.Remotes.Effects.OnServerEvent:Connect(TweakEffect) + + +local function ExplodePart(player,part) + local player = GetPlayer(player) + if + not player:CheckFloodGate(1) or + not (player:OwnsPart(part) or + player:OwnsPart(part.Parent)) + then + return + end + _G.Methods.explode(part) +end +_G.Remotes.Explode.OnServerEvent:Connect(ExplodePart) + +local function DeletePart(player,part,destruct) + local player = GetPlayer(player) + if + part and + player:CheckFloodGate(1) and + player:OwnsPart(part) + then + if destruct then + for _,part in pairs(part:GetChildren()) do + if part:IsA("BasePart") then + for _,joint in pairs(part:GetJoints()) do + if joint.Name == "BuiltWeldConstraint" then joint:Destroy() end + end + end + end + else + part:Destroy() + end + end +end +_G.Remotes.Delete.OnServerInvoke = DeletePart + +local function ClearParts(player) + local player = GetPlayer(player) + if not player:CheckFloodGate(1) then return end + for _,part in pairs(player:GetParts()) do + part:Destroy() + end +end +_G.Remotes.Clear.OnServerInvoke = ClearParts + +local function ShareParts(player,id,parts) + local player = GetPlayer(player) + if not player:CheckFloodGate(1) then return end + if not parts then parts = player:GetParts() end + for _,part in pairs(parts) do + _G.Storage.TeamBox:Clone().Parent = part + part.TeamBox.Adornee = part + _G.Debris:AddItem(part.TeamBox,0.5) + if not id then + part:AddTag(players.public.parttag) + else + part:AddTag(GetPlayer(id).parttag) + end + end +end +_G.Remotes.Collaborate.OnServerInvoke = ShareParts + +local function EnableCombat(player,parts) + local player = GetPlayer(player) + if not player:CheckFloodGate(1) then return end + if not parts then parts = player:GetParts() end + for _,part in pairs(parts) do + _G.Storage.DangerBox:Clone().Parent = part + part.DangerBox.Adornee = part + _G.Debris:AddItem(part.DangerBox,0.5) + part:AddTag("Combat") + end +end +_G.Remotes.Combat.OnServerInvoke = EnableCombat + +local function SaveVehicle(player,data,key,group) + player = GetPlayer(player) + player:EvaluateSaves() + if ((key ~= "autosave") and (player.max_saves < tonumber(key))) then + player:Notify("Save Error","You do not have enough space.") + return + end + if not player:OwnsSave(group) then return end + local success,value = SetStoreData(players[group].stores.saves,key,data) + if not success then player:Notify("Vehicle Save Error",value) error(value) return end + local length = #data + local suffix = "" + if length > 5000 then suffix = "K" length = math.floor(length/1000) end + player:Notify("Vehicle Saved","Save success, remember to save regularly! Size: "..length..suffix.."C") + players[group].saves[key] = data + return success +end +_G.Remotes.Save.OnServerInvoke = SaveVehicle + +local function GetSaveVersionInfo(group,key) + local store = players[group].stores.saves + local listSuccess, pages = pcall(function() + return store:ListVersionsAsync(key) + end) + local result = {} + if listSuccess then + local items + while true do + items = pages:GetCurrentPage() + for key, info in pairs(items) do + if not info.IsDeleted then + result[info.Version] = os.date("%c",info.CreatedTime/1000) + end + end + if pages.IsFinished then break else pages:AdvanceToNextPageAsync() end + end + else + warn(pages) + end + return result +end + +SAVE_INFO_EMPTY = "Empty" +EMPTY_SAVE_INFO = {autosave = SAVE_INFO_EMPTY} +for i=1,DEFAULT_MAX_SAVES do EMPTY_SAVE_INFO[tostring(i)] = SAVE_INFO_EMPTY end +local function GetSaveInfo(player,group) + player = GetPlayer(player) + group = group or player.userid + local key + if group:match("(.-):(.+)") then group,key = group:match("(.-):(.+)") end + if not player:OwnsSave(group) then return end + local max_saves = DEFAULT_MAX_SAVES + if group == player.userid then player:EvaluateSaves() max_saves = player.max_saves end + if key then + return GetSaveVersionInfo(group,key) + else + local success,data = GetStoreData(players[group].stores.save_info,"info") + if not success then + player:Notify("Save Info Error: ","Your save info failed to load, this is a fatal error, try rejoining.") + return + end + local save_info = data or {} + if not save_info.autosave then save_info.autosave = "Unused" end + for index,item in ipairs(save_info) do + local data = save_info[index] + save_info[index] = nil + save_info[tostring(index)] = data + end + for i = 1,max_saves do if not save_info[tostring(i)] then save_info[tostring(i)] = "Empty" end end -- fill any missing slots + return save_info + end +end +_G.Remotes.GetSaveInfo.OnServerInvoke = GetSaveInfo + +local function SetSaveInfo(player,data,group) + player = GetPlayer(player) + if not player:OwnsSave(group) then return end + player:EvaluateSaves() + for index,item in pairs(data) do + if not tonumber(index) and index ~= "autosave" then + player:Notify("Malformed save data! Invalid string: "..index) + return + end + end + if #data > player.max_saves then + player:Notify("You do not have enough save slots! "..tostring(#data-1)) + return + end + SetStoreData(players[group].stores.save_info,"info",data) + return data +end +_G.Remotes.SetSaveInfo.OnServerInvoke = SetSaveInfo + +local function LoadPlayerSave(player,...) + return GetPlayer(player):LoadSave(...) +end +_G.Remotes.Load.OnServerInvoke = LoadPlayerSave + +local function ConfigurePlayerVehicle(player,model) + for _,part in pairs(model:GetChildren()) do + ConfigurePart(player,part) + end +end + +local function PlaceVehicle(player,cframe,key,group) + local player = GetPlayer(player) + if not player:CheckFloodGate(3) then return end + for _,part in pairs(_G.Tags:GetTagged("P_"..player.userid)) do + part:Destroy() + end + local raw = player:LoadSave(group,key) + if not raw then return end + local model = _G.Methods.saves.dataToModel(raw,true) + if not model then return end + model.Parent = game.Workspace + local primary = model:FindFirstChildWhichIsA("VehicleSeat",true) + if primary then + model.PrimaryPart = primary + end + model:PivotTo(cframe) + --[[task.defer(function() + if primary then _G.Methods.setNetworkOwner(primary,player.instance) end + end)]] + ConfigurePlayerVehicle(player,model) +end +_G.Remotes.Place.OnServerInvoke = PlaceVehicle + +-- i hate this... +local function PlaceSubassembly(player,cframe,data,connection) + local player = GetPlayer(player) + if not player:CheckFloodGate(10) then return end + local model = _G.Methods.saves.dataToModel(data,true) + model:PivotTo(cframe) + if player:OwnsPart(connection) then Weld(model.PrimaryPart,connection) end + ConfigurePlayerVehicle(player,model) +end +_G.Remotes.PlaceSubassembly.OnServerInvoke = PlaceSubassembly + +local function PlaySoundEffect(player,sound) + if not GetPlayer(player):CheckFloodGate(1) then return end + sound:Play() +end +_G.Remotes.PlaySoundEffect.OnServerEvent:Connect(PlaySoundEffect) + +local function ChangeSoundEffect(player,sound,volume,pitch) + if not GetPlayer(player):CheckFloodGate(1) then return end + if sound then + if volume then sound.Volume = volume end + if pitch then sound.Pitch = pitch end + end +end +_G.Remotes.ChangeSoundEffect.OnServerEvent:Connect(ChangeSoundEffect) + +local function Paint(player,part,colour) -- Colour is spelt with a 'u' (pun intended) + player = GetPlayer(player) + if not player:CheckFloodGate(1) then return end + if not player:OwnsPart(part) then return end + _G.Methods.colorPart(part,colour) +end +_G.Remotes.Paint.OnServerEvent:Connect(Paint) + +local function Admin(player) + player = GetPlayer(player) + if not player:CheckFloodGate(1) then return end + return player:IsAdmin() +end +_G.Remotes.Admin.OnServerInvoke = Admin + +local function SnapPart(player,weld) + local player = GetPlayer(player) + if not player:CheckFloodGate(1) then return end + if weld.Parent and (player:OwnsPart(weld.Parent) or player:OwnsPart(weld.Parent.Parent)) and weld.Name == "BuiltWeldConstraint" then + weld:Destroy() + if weld.Part0 and weld.Part0.Parent:FindFirstChild("BombScript") then weld.Part0.Parent.BombScript.Enabled = true end + if weld.Part1 and weld.Part1.Parent:FindFirstChild("BombScript") then weld.Part1.Parent.BombScript.Enabled = true end + else + print("failed",weld:GetFullName()) + end +end +_G.Remotes.Snap.OnServerEvent:Connect(SnapPart) + +local function EditPart(player,part,edit,value) + if not GetPlayer(player):CheckFloodGate(1) then return end + if GetPlayer(player):OwnsPart(part) then + if edit == "Size" then + ResizePart(part,value) + end + if _G.IsNumber(value) then + local min = edit:GetAttribute("Lower") or -math.huge + local max = edit:GetAttribute("Upper") or math.huge + edit.Value = math.clamp(value,min,max) + else + edit.Value = value + end + objectModule.update(part) + --[[local modules = objectInteraction.modulesByName[part.Name] or {} + for _,module in pairs(modules) do + if module.setup then module.setup(part) end + end]] + end +end +_G.Remotes.Edit.OnServerInvoke = EditPart + +--_G.Remotes.Sell + +commands.new("get user id (.+)","get user id : Get's a player's userid by name.",function(player,name) + local player = GetPlayer(player) + if name then + player:Notify("Get User Id",game.Players:GetUserIdFromNameAsync(name)) + end +end) +commands.new("open player (.+)","open player : Load a player's data on the server.",function(player,userid) + local player = GetPlayer(player) + if not player:IsAdmin() then player:Kick("Security") return end + if userid then + local name = game.Players:GetNameFromUserIdAsync(tonumber(userid)) + AddPlayer({UserId = tonumber(userid),DisplayName = name,Name = name}) + player:Notify("Loading Virtual Player",name) + end +end) +commands.new("set save ([^%s]+) (%d+) ([^%s+]) data:(.*)","set store : Set the save slot data.",function(player,store,userid,key,value) + local player = GetPlayer(player) + if not player:IsAdmin() then player:Kick("Security") return end + print("Modifying store: ",store,userid,key,value) + SetStoreData(players[userid].stores.saves,key,value) + player:Notify("Set Store Value","Success?") +end) +commands.new("read player stats%((.-)%)","",function(player,userid) + local player = GetPlayer(player) + if userid then + local other = GetPlayer(tostring(userid)) + player:Notify(other.instance.DisplayName.." Statistics",_G.Http:JSONEncode(other.stats)) + end +end) +commands.new("restart server","restart server: Update to latest version.",function(player,userid) + local player = GetPlayer(player) + if not player:IsAdmin() then player:Kick("Security") return end + _G.Remotes.Notification:FireAllClients("Server Restarting","Hold tight!") + game:GetService('TeleportService'):TeleportPartyAsync(137149259787276,game.Players:GetPlayers(),{moveTo = game.PlaceId}) +end) +commands.new("rejoin","rejoin: Makes you rejoin.",function(player,userid) + local player = GetPlayer(player) + _G.Remotes.Notification:FireClient(player.instance,"Rejoining","Hold tight!") + game:GetService('TeleportService'):TeleportAsync(game.PlaceId,{player.instance}) +end) + + +--[[local store,userid,key,value = message:match("setstore%((.-),(%d-),(.-),(.-)%)") + if store then + if player then + print("Modifying store: ",store,userid,key,value) + player.stores.saves:SetAsync(key,value) + end + end]] + +local function SetMotor(character,motor,C0,C1) -- Allow clients to adjust their character motors + if not character:FindFirstChildWhichIsA("Humanoid") then + character = character.Character + end + if + not character or + not motor or + not motor:IsDescendantOf(character) + then + return + end + + motor.C0 = C0 motor.C1 = C1 +end +_G.Remotes.MotorMoveClient.OnServerEvent:Connect(SetMotor) + +local textService = game:GetService("TextService") +local function Chat(instance,message) + local player = GetPlayer(instance) + if not player:CheckFloodGate(5) then return end + message = textService:FilterStringAsync(message,instance.UserId,Enum.TextFilterContext.PublicChat):GetNonChatStringForBroadcastAsync() + _G.Remotes.Chat:FireAllClients(instance.DisplayName.." | "..instance.Name,message,player.stats.color) + game.Chat:Chat(instance.Character,message) +end +_G.Remotes.Chat.OnServerEvent:Connect(Chat) + +local function ApplyState(player,part,...) + local player = GetPlayer(player) + if not player:CheckFloodGate(2) then return end + if not player:OwnsPart(part) then return end + local state = partStateModule.new() + state.owner = player.instance + state.part = part + state:deserialise(...) + state:apply() +end +_G.Remotes.Apply.OnServerEvent:Connect(ApplyState) + +spawn(function() -- Drain all flood gates every heartbeat + while wait() do + for id,player in pairs(players) do + if _G.IsString(id) then player:DrainFloodGate(PLAYER_FLOOD_TICK) end + end + end +end) + +spawn(function() -- Make the game rain for players every now and then + while true do + _G.Remotes.Rain:FireAllClients(false) + wait(math.random(1,720)) + _G.Remotes.Rain:FireAllClients(true) + wait(math.random(1,60)) + end +end) \ No newline at end of file diff --git a/scripts/server/Terrain6Old b/scripts/server/Terrain6Old new file mode 100644 index 0000000..ca1b54e --- /dev/null +++ b/scripts/server/Terrain6Old @@ -0,0 +1,90 @@ +local CHUNK_SIZE = 256 +local RADIUS = 3 +local LEEWAY = 20 +local POOL_SIZE = 64 +local BASE_RADIUS = 1 + +script:WaitForChild("GenerateChunk").Disabled = true +local pool = 0 +local chunks = {} +local schunks = {} +local function chunk(x,z) + if not schunks[x] then schunks[x] = {} end + local i = {x=x,z=z} + local first,second + local c = {delete=function() + if first then + game.Workspace.Terrain:FillRegion(Region3.new(first.Value,second.Value),4,Enum.Material.Air) + end + end,viable = true} + chunks[i]=c + schunks[x][z]=c + + task.defer(function() + repeat wait() until pool < POOL_SIZE + if not c.viable and not c.permanent then return end + local actor = Instance.new("Actor") + actor.Parent = game.Workspace + local new_script = script.GenerateChunk:Clone() + local location = new_script.Location + location.Value = Vector3.new(x,CHUNK_SIZE,z) + first = new_script.First + second = new_script.Second + new_script.Parent = actor + new_script.Disabled = false + pool = pool + 1 + new_script.Destroying:Connect(function() + pool = pool - 1 + end) + end) + + return c +end + +for x = -BASE_RADIUS,BASE_RADIUS do + for z = -BASE_RADIUS,BASE_RADIUS do + if Vector2.new(x,z).Magnitude < BASE_RADIUS then + chunk(x,z).permanent = true + end + end +end + +while wait(0.5) do + for index,chunk in pairs(chunks) do + chunk.viable = false + end + local perPlayer = function(p) + for index,chunk in pairs(chunks) do + if Vector2.new(p.x-index.x*CHUNK_SIZE,p.z-index.z*CHUNK_SIZE).Magnitude < CHUNK_SIZE*(RADIUS+LEEWAY) then + chunk.viable = true + end + end + for x=math.ceil(p.x/CHUNK_SIZE)-RADIUS,math.ceil(p.x/CHUNK_SIZE)+RADIUS do + for z=math.ceil(p.z/CHUNK_SIZE)-RADIUS,math.ceil(p.z/CHUNK_SIZE)+RADIUS do + if not schunks[x] then schunks[x] = {} end + if not schunks[x][z] then + chunk(x,z) + end + end + end + end + + for _,player in pairs(game.Players:GetPlayers()) do + if player.Character then + if player.Character.PrimaryPart then + perPlayer(player.Character.PrimaryPart.Position) + end + end + end + + perPlayer(game.Workspace.CurrentCamera.CFrame.Position) + + for index,chunk in pairs(chunks) do + if not chunk.viable and not chunk.permanent then + schunks[index.x][index.z] = nil + --if #schunks[index.x] == 0 then schunks[index.x] = nil end + chunk.delete() + chunks[index] = nil + end + end +end \ No newline at end of file