sm64-roblox-liberty-prime/sm64/init.client.lua
2022-12-03 04:15:52 -06:00

518 lines
13 KiB
Lua

--!strict
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local StarterGui = game:GetService("StarterGui")
local TweenService = game:GetService("TweenService")
local UserInputService = game:GetService("UserInputService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local ContextActionService = game:GetService("ContextActionService")
local Sounds = require(script.Sounds)
local Enums = require(script.Enums)
local Mario = require(script.Mario)
local Types = require(script.Types)
local Util = require(script.Util)
local Action = Enums.Action
local Buttons = Enums.Buttons
local InputFlags = Enums.InputFlags
local ParticleFlags = Enums.ParticleFlags
type InputType = Enum.UserInputType | Enum.KeyCode
type Controller = Types.Controller
type Mario = Mario.Class
local player: Player = assert(Players.LocalPlayer)
local STEP_RATE = 30
local PARTICLE_CLASSES = {
Fire = true,
Smoke = true,
Sparkles = true,
ParticleEmitter = true,
}
local FLIP = CFrame.Angles(0, math.pi, 0)
-------------------------------------------------------------------------------------------------------------------------------------------------
-- Input Driver
-------------------------------------------------------------------------------------------------------------------------------------------------
local MATH_TAU = math.pi * 2
local BUTTON_FEED: { Enum.UserInputState } = {}
local BUTTON_A = "BTN_" .. Buttons.A_BUTTON
local function toStrictNumber(str: string): number
local result = tonumber(str)
return assert(result, "Invalid number!")
end
local function processAction(id: string, state: Enum.UserInputState)
if id == "MarioDebug" then
if state == Enum.UserInputState.Begin then
local isDebug = not script.Util:GetAttribute("Debug")
local character = player.Character
-- stylua: ignore
local rootPart = if character
then character.PrimaryPart
else nil
if rootPart then
local action = rootPart:FindFirstChild("Action")
if action and action:IsA("BillboardGui") then
action.Enabled = isDebug
end
end
script.Util:SetAttribute("Debug", isDebug)
end
else
local button = toStrictNumber(id:sub(5))
BUTTON_FEED[button] = state
end
end
local function bindInput(button: number, label: string, ...: InputType)
local id = "BTN_" .. button
ContextActionService:BindAction(id, processAction, true, ...)
if UserInputService.TouchEnabled then
ContextActionService:SetTitle(id, label)
end
end
local function updateCollisions()
for i, player in Players:GetPlayers() do
assert(player:IsA("Player"))
-- stylua: ignore
local rootPart = if player.Character
then player.Character.PrimaryPart
else nil
if rootPart then
local parts = rootPart:GetConnectedParts(true)
for i, part in parts do
if part:IsA("BasePart") then
part.CanCollide = false
end
end
end
end
end
local function updateController(controller: Controller, humanoid: Humanoid)
local moveDir = humanoid.MoveDirection
local pos = Vector2.new(moveDir.X, -moveDir.Z)
local len = math.min(1, pos.Magnitude)
controller.StickMag = len * 64
controller.StickX = pos.X * 64
controller.StickY = pos.Y * 64
humanoid:ChangeState(Enum.HumanoidStateType.Physics)
controller.ButtonPressed:Clear()
if humanoid.Jump then
BUTTON_FEED[Buttons.A_BUTTON] = Enum.UserInputState.Begin
humanoid.Jump = false
elseif controller.ButtonDown:Has(Buttons.A_BUTTON) then
BUTTON_FEED[Buttons.A_BUTTON] = Enum.UserInputState.End
end
for button, state in pairs(BUTTON_FEED) do
if state == Enum.UserInputState.Begin then
controller.ButtonDown:Add(button)
controller.ButtonPressed:Add(button)
elseif state == Enum.UserInputState.End then
controller.ButtonDown:Remove(button)
end
end
table.clear(BUTTON_FEED)
end
ContextActionService:BindAction("MarioDebug", processAction, false, Enum.KeyCode.P)
bindInput(Buttons.B_BUTTON, "B", Enum.UserInputType.MouseButton1, Enum.KeyCode.ButtonX)
bindInput(Buttons.Z_TRIG, "Z", Enum.KeyCode.LeftShift, Enum.KeyCode.RightShift, Enum.KeyCode.ButtonL2)
-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Network Dispatch
-------------------------------------------------------------------------------------------------------------------------------------------------------------
local Commands = {}
local lazyNetwork = ReplicatedStorage:WaitForChild("LazyNetwork")
assert(lazyNetwork:IsA("RemoteEvent"), "bad lazyNetwork!")
function Commands.PlaySound(player: Player, name: string)
local sound: Sound? = Sounds[name]
local character = player.Character
-- stylua: ignore
local rootPart = if character
then character.PrimaryPart
else nil
if rootPart and sound then
local oldSound: Instance? = rootPart:FindFirstChild(name)
if oldSound and oldSound:IsA("Sound") and name:find("MARIO") then
oldSound.TimePosition = 0
else
local newSound: Sound = sound:Clone()
newSound.Parent = rootPart
newSound:Play()
newSound.Ended:Connect(function()
newSound:Destroy()
end)
end
end
end
function Commands.SetParticle(player: Player, name: string, set: boolean)
local character = player.Character
-- stylua: ignore
local rootPart = if character
then character.PrimaryPart
else nil
if rootPart then
local particles = rootPart:FindFirstChild("Particles")
-- stylua: ignore
local inst = if particles
then particles:FindFirstChild(name)
else nil
if inst and PARTICLE_CLASSES[inst.ClassName] then
local particle = inst :: ParticleEmitter
local emit = particle:GetAttribute("Emit")
if typeof(emit) == "number" then
particle:Emit(emit)
elseif set ~= nil then
particle.Enabled = set
end
end
end
end
function Commands.SetAngle(player: Player, angle: Vector3int16)
local character = player.Character
-- stylua: ignore
local waist = if character
then character:FindFirstChild("Waist", true)
else nil
if waist and waist:IsA("Motor6D") then
local props = { C1 = Util.ToRotation(-angle) + waist.C1.Position }
local tween = TweenService:Create(waist, TweenInfo.new(0.1), props)
tween:Play()
end
end
local function processCommand(player: Player, cmd: string, ...: any)
local command = Commands[cmd]
if command then
task.spawn(command, player, ...)
else
warn("Unknown Command:", cmd, ...)
end
end
local function networkDispatch(cmd: string, ...: any)
lazyNetwork:FireServer(cmd, ...)
processCommand(player, cmd, ...)
end
local function onNetworkReceive(target: Player, cmd: string, ...: any)
if target ~= player then
processCommand(target, cmd, ...)
end
end
lazyNetwork.OnClientEvent:Connect(onNetworkReceive)
-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- Mario Driver
-------------------------------------------------------------------------------------------------------------------------------------------------------------
local lastUpdate = os.clock()
local lastAngle: Vector3int16?
local mario: Mario = Mario.new()
local controller = mario.Controller
local enumMap = {}
local goalCF: CFrame
local activeTrack: AnimationTrack?
local peakSpeed = 0
local emptyId = ""
local reset = Instance.new("BindableEvent")
reset.Archivable = false
reset.Parent = script
reset.Name = "Reset"
if RunService:IsStudio() then
local dummySequence = Instance.new("KeyframeSequence")
local provider = game:GetService("KeyframeSequenceProvider")
emptyId = provider:RegisterKeyframeSequence(dummySequence)
end
while not player.Character do
player.CharacterAdded:Wait()
end
local character = assert(player.Character)
local pivot = character:GetPivot().Position
mario.Position = Util.ToSM64(pivot)
local function onReset()
local roblox = Vector3.yAxis * 100
local sm64 = Util.ToSM64(roblox)
local char = player.Character
if char then
local reset = char:FindFirstChild("Reset")
local cf = CFrame.new(roblox)
if reset and reset:IsA("RemoteEvent") then
reset:FireServer()
end
char:PivotTo(cf)
end
mario.SlideVelX = 0
mario.SlideVelZ = 0
mario.ForwardVel = 0
mario.IntendedYaw = 0
mario.Position = sm64
mario.Velocity = Vector3.zero
mario.FaceAngle = Vector3int16.new()
mario:SetAction(Action.SPAWN_SPIN_AIRBORNE)
end
local function update()
local character = player.Character
if not character then
return
end
local now = os.clock()
local gfxRot = CFrame.identity
-- stylua: ignore
local humanoid = if character
then character:FindFirstChildOfClass("Humanoid")
else nil
local simSpeed = tonumber(script:GetAttribute("TimeScale") or nil) or 1
local frames = math.floor((now - lastUpdate) * (STEP_RATE * simSpeed))
if frames > 0 and humanoid then
lastUpdate = now
updateCollisions()
for i = 1, math.min(4, frames) do
updateController(mario.Controller, humanoid)
mario:ExecuteAction()
end
local pos = Util.ToRoblox(mario.Position)
local rot = Util.ToRotation(mario.FaceAngle)
gfxRot = Util.ToRotation(mario.GfxAngle)
goalCF = CFrame.new(pos) * FLIP * gfxRot
end
local interp = math.min(1, simSpeed / 2)
if character and goalCF then
local cf = character:GetPivot()
local rootPart = character.PrimaryPart
local animator = character:FindFirstChildWhichIsA("Animator", true)
if animator and (mario.AnimDirty or mario.AnimReset) and mario.AnimFrame >= 0 then
local anim = mario.AnimCurrent
local animSpeed = 0.1 / simSpeed
if activeTrack and (activeTrack.Animation ~= anim or mario.AnimReset) then
activeTrack:Stop(animSpeed)
activeTrack = nil
end
if not activeTrack and anim then
if anim.AnimationId == "" then
if RunService:IsStudio() then
warn("!! FIXME: Empty AnimationId for", anim.Name, "will break in live games!")
end
anim.AnimationId = emptyId
end
local track = animator:LoadAnimation(anim)
track:Play(animSpeed, 1, 0)
activeTrack = track
end
if activeTrack then
local speed = mario.AnimAccel / 0x10000
if speed > 0 then
activeTrack:AdjustSpeed(speed * simSpeed)
else
activeTrack:AdjustSpeed(simSpeed)
end
end
mario.AnimDirty = false
mario.AnimReset = false
end
if activeTrack and mario.AnimSetFrame > -1 then
activeTrack.TimePosition = mario.AnimSetFrame / STEP_RATE
mario.AnimSetFrame = -1
end
if rootPart then
local action = rootPart:FindFirstChild("Action")
local particles = rootPart:FindFirstChild("Particles")
local alignPos = rootPart:FindFirstChildOfClass("AlignPosition")
local alignCF = rootPart:FindFirstChildOfClass("AlignOrientation")
local throw = mario.ThrowMatrix
if throw then
local pos = Util.ToRoblox(throw.Position)
goalCF = throw.Rotation * FLIP + pos
end
if alignCF then
cf = cf:Lerp(goalCF, interp)
alignCF.CFrame = cf.Rotation
end
-- stylua: ignore
local debugLabel = if action
then action:FindFirstChildOfClass("TextLabel")
else nil
if debugLabel then
local actionId = mario.Action()
-- stylua: ignore
local anim = if activeTrack
then activeTrack.Animation
else nil
-- stylua: ignore
local animName = if anim
then anim.Name
else nil
local debugText = "Action: "
.. Enums.GetName(Action, actionId)
.. "\n"
.. "Animation: "
.. tostring(animName)
.. "\n"
.. "ForwardVel: "
.. string.format("%.2f", mario.ForwardVel)
debugLabel.Text = debugText
end
if alignPos then
alignPos.Position = cf.Position
end
local bodyState = mario.BodyState
local action = mario.Action()
if action ~= Action.BUTT_SLIDE and action ~= Action.WALKING then
bodyState.TorsoAngle *= 0
end
local ang = bodyState.TorsoAngle
if ang ~= lastAngle then
networkDispatch("SetAngle", ang)
lastAngle = ang
end
if particles then
for name, flag in pairs(ParticleFlags) do
local inst = particles:FindFirstChild(name)
if inst and PARTICLE_CLASSES[inst.ClassName] then
local name = inst.Name
local particle = inst :: ParticleEmitter
local emit = particle:GetAttribute("Emit")
local hasFlag = mario.ParticleFlags:Has(flag)
if emit then
if hasFlag then
networkDispatch("SetParticle", name)
end
elseif particle.Enabled ~= hasFlag then
networkDispatch("SetParticle", name, hasFlag)
end
end
end
end
for name: string, sound: Sound in pairs(Sounds) do
local looped = false
if sound:IsA("Sound") then
if sound.TimeLength == 0 then
continue
end
looped = sound.Looped
end
if sound:GetAttribute("Play") then
networkDispatch("PlaySound", sound.Name)
if not looped then
sound:SetAttribute("Play", false)
end
elseif looped then
sound:Stop()
end
end
character:PivotTo(cf)
end
end
end
reset.Event:Connect(onReset)
RunService.Heartbeat:Connect(update)
while task.wait(1) do
local success = pcall(function()
return StarterGui:SetCore("ResetButtonCallback", reset)
end)
if success then
break
end
end
-------------------------------------------------------------------------------------------------------------------------------------------------------------