sm64-roblox-liberty-prime/client/init.client.lua
Max ba0e5364bb Major restructuring and bug fixes.
Moved a lot of things around, fixed a lot of bugs, made the animations and sounds public. Special thanks to CuckyDev for helping me track down all the small problems that were lingering, and for fixing a major issue with the simulation rate. There's still some stuff to fix and improve, but now this should be more portable and useable by the wider community! 🎉

Co-Authored-By: Regan Green <cuckydev@gmail.com>
2023-07-07 22:01:02 -05:00

649 lines
16 KiB
Lua

--!strict
local Core = script.Parent
if Core:GetAttribute("HotLoading") then
task.wait(3)
end
for i, desc in script:GetDescendants() do
if desc:IsA("BaseScript") then
desc.Enabled = true
end
end
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 Shared = require(Core.Shared)
local Sounds = Shared.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 MarioFlags = Enums.MarioFlags
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 FLIP = CFrame.Angles(0, math.pi, 0)
local STEP_RATE = 30
local NULL_TEXT = `<font color="#FF0000">NULL</font>`
local debugStats = Instance.new("BoolValue")
debugStats.Name = "DebugStats"
debugStats.Archivable = false
debugStats.Parent = game
local PARTICLE_CLASSES = {
Fire = true,
Smoke = true,
Sparkles = true,
ParticleEmitter = true,
}
local AUTO_STATS = {
"Position",
"Velocity",
"AnimFrame",
"FaceAngle",
"ActionState",
"ActionTimer",
"ActionArg",
"ForwardVel",
"SlideVelX",
"SlideVelZ",
"CeilHeight",
"FloorHeight",
}
local ControlModule: {
GetMoveVector: (self: any) -> Vector3,
}
while not ControlModule do
local inst = player:FindFirstChild("ControlModule", true)
if inst then
ControlModule = (require :: any)(inst)
end
task.wait(0.1)
end
-------------------------------------------------------------------------------------------------------------------------------------------------
-- Input Driver
-------------------------------------------------------------------------------------------------------------------------------------------------
-- NOTE: I had to replace the default BindAction via KeyCode and UserInputType
-- BindAction forces some mappings (such as R2 mapping to MouseButton1) which you
-- can't turn off otherwise.
local BUTTON_FEED = {}
local BUTTON_BINDS = {}
local function toStrictNumber(str: string): number
local result = tonumber(str)
return assert(result, "Invalid number!")
end
local function processAction(id: string, state: Enum.UserInputState, input: InputObject)
if id == "MarioDebug" and Core:GetAttribute("DebugToggle") then
if state == Enum.UserInputState.Begin then
local character = player.Character
if character then
local isDebug = not character:GetAttribute("Debug")
character:SetAttribute("Debug", isDebug)
end
end
else
local button = toStrictNumber(id:sub(5))
BUTTON_FEED[button] = state
end
end
local function processInput(input: InputObject, gameProcessedEvent: boolean)
if gameProcessedEvent then
return
end
if BUTTON_BINDS[input.UserInputType] ~= nil then
processAction(BUTTON_BINDS[input.UserInputType], input.UserInputState, input)
end
if BUTTON_BINDS[input.KeyCode] ~= nil then
processAction(BUTTON_BINDS[input.KeyCode], input.UserInputState, input)
end
end
UserInputService.InputBegan:Connect(processInput)
UserInputService.InputChanged:Connect(processInput)
UserInputService.InputEnded:Connect(processInput)
local function bindInput(button: number, label: string, ...: InputType)
local id = "BTN_" .. button
if UserInputService.TouchEnabled then
ContextActionService:BindAction(id, processAction, true)
ContextActionService:SetTitle(id, label)
end
for i, input in { ... } do
BUTTON_BINDS[input] = id
end
end
local function updateCollisions()
for i, player in Players:GetPlayers() do
-- stylua: ignore
local character = player.Character
local rootPart = character and character.PrimaryPart
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?)
if not humanoid then
return
end
local moveDir = ControlModule:GetMoveVector()
local pos = Vector2.new(moveDir.X, -moveDir.Z)
local mag = 0
if pos.Magnitude > 0 then
if pos.Magnitude > 1 then
pos = pos.Unit
end
mag = pos.Magnitude
end
controller.StickMag = mag * 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
elseif controller.ButtonDown:Has(Buttons.A_BUTTON) then
BUTTON_FEED[Buttons.A_BUTTON] = Enum.UserInputState.End
end
local character = humanoid.Parent
local lastButtonValue = controller.ButtonDown()
for button, state in pairs(BUTTON_FEED) do
if state == Enum.UserInputState.Begin then
controller.ButtonDown:Add(button)
elseif state == Enum.UserInputState.End then
controller.ButtonDown:Remove(button)
end
end
local buttonValue = controller.ButtonDown()
controller.ButtonPressed:Set(buttonValue)
table.clear(BUTTON_FEED)
if character and character:GetAttribute("TAS") then
return
end
if Core:GetAttribute("ToolAssistedInput") then
return
end
local diff = bit32.bxor(buttonValue, lastButtonValue)
controller.ButtonPressed:Band(diff)
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,
Enum.KeyCode.ButtonR2
)
-------------------------------------------------------------------------------------------------------------------------------------------------------------
-- 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
local rootPart = character and character.PrimaryPart
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
local rootPart = character and character.PrimaryPart
if rootPart then
local particles = rootPart:FindFirstChild("Particles")
local inst = particles and particles:FindFirstChild(name)
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
local waist = character and character:FindFirstChild("Waist", true)
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
function Commands.SetCamera(player: Player, cf: CFrame?)
local camera = workspace.CurrentCamera
if cf ~= nil then
camera.CameraType = Enum.CameraType.Scriptable
camera.CFrame = cf
else
camera.CameraType = Enum.CameraType.Custom
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 subframe = 0 -- 30hz subframe
local emptyId = ""
local goalCF: CFrame
local prevCF: CFrame
local activeTrack: AnimationTrack?
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()
mario.Position = Util.ToSM64(pivot.Position)
goalCF = pivot
prevCF = pivot
local function setDebugStat(key: string, value: any)
if typeof(value) == "Vector3" then
value = string.format("%.3f, %.3f, %.3f", value.X, value.Y, value.Z)
elseif typeof(value) == "Vector3int16" then
value = string.format("%i, %i, %i", value.X, value.Y, value.Z)
elseif type(value) == "number" then
value = string.format("%.3f", value)
end
debugStats:SetAttribute(key, value)
end
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)
char:PivotTo(cf)
goalCF = cf
prevCF = cf
if reset and reset:IsA("RemoteEvent") then
reset:FireServer()
end
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 scale = character:GetScale()
Util.Scale = scale / 16 -- HACK! Should this be instanced?
local pos = character:GetPivot().Position
local dist = (Util.ToRoblox(mario.Position) - pos).Magnitude
local humanoid = character:FindFirstChildOfClass("Humanoid")
if dist > (scale * 20) then
mario.Position = Util.ToSM64(pos)
end
local simSpeed = tonumber(character:GetAttribute("TimeScale") or nil) or 1
subframe += (now - lastUpdate) * (STEP_RATE * simSpeed)
lastUpdate = now
if character:GetAttribute("WingCap") or Core:GetAttribute("WingCap") then
mario.Flags:Add(MarioFlags.WING_CAP)
mario.Flags:Remove(MarioFlags.NORMAL_CAP)
else
mario.Flags:Add(MarioFlags.NORMAL_CAP)
mario.Flags:Remove(MarioFlags.WING_CAP)
end
subframe = math.min(subframe, 4) -- Prevent execution runoff
while subframe >= 1 do
subframe -= 1
updateCollisions()
updateController(mario.Controller, humanoid)
mario:ExecuteAction()
local gfxPos = Util.ToRoblox(mario.Position)
gfxRot = Util.ToRotation(mario.GfxAngle)
prevCF = goalCF
goalCF = CFrame.new(gfxPos) * FLIP * gfxRot
end
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
if tostring(activeTrack.Animation) == "TURNING_PART1" then
if anim and anim.Name == "TURNING_PART2" then
mario.AnimSkipInterp = 2
animSpeed *= 2
end
end
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 particles = rootPart:FindFirstChild("Particles")
local alignPos = rootPart:FindFirstChildOfClass("AlignPosition")
local alignCF = rootPart:FindFirstChildOfClass("AlignOrientation")
local actionId = mario.Action()
local throw = mario.ThrowMatrix
if throw then
local throwPos = Util.ToRoblox(throw.Position)
goalCF = throw.Rotation * FLIP + throwPos
end
if alignCF then
local nextCF = prevCF:Lerp(goalCF, subframe)
-- stylua: ignore
cf = if mario.AnimSkipInterp > 0
then cf.Rotation + nextCF.Position
else nextCF
alignCF.CFrame = cf.Rotation
end
local isDebug = character:GetAttribute("Debug")
local limits = character:GetAttribute("EmulateLimits")
script.Util:SetAttribute("Debug", isDebug)
debugStats.Value = isDebug
if limits ~= nil then
Core:SetAttribute("TruncateBounds", limits)
end
if isDebug then
local animName = activeTrack and tostring(activeTrack.Animation)
setDebugStat("Animation", animName)
local actionName = Enums.GetName(Action, actionId)
setDebugStat("Action", actionName)
local wall = mario.Wall
setDebugStat("Wall", wall and wall.Instance.Name or NULL_TEXT)
local floor = mario.Floor
setDebugStat("Floor", floor and floor.Instance.Name or NULL_TEXT)
local ceil = mario.Ceil
setDebugStat("Ceiling", ceil and ceil.Instance.Name or NULL_TEXT)
end
for _, name in AUTO_STATS do
local value = rawget(mario :: any, name)
setDebugStat(name, value)
end
if alignPos then
alignPos.Position = cf.Position
end
local bodyState = mario.BodyState
local ang = bodyState.TorsoAngle
if actionId ~= Action.BUTT_SLIDE and actionId ~= Action.WALKING then
bodyState.TorsoAngle *= 0
end
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 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
-------------------------------------------------------------------------------------------------------------------------------------------------------------