--!strict local Mario = {} Mario.__index = Mario local SM64 = script.Parent local Core = SM64.Parent local Util = require(SM64.Util) local Enums = require(SM64.Enums) local Shared = require(Core.Shared) local Sounds = Shared.Sounds local Animations = Shared.Animations local Types = require(SM64.Types) local Flags = Types.Flags local Action = Enums.Action local Buttons = Enums.Buttons local ActionFlags = Enums.ActionFlags local ActionGroups = Enums.ActionGroups local MarioCap = Enums.MarioCap local MarioEyes = Enums.MarioEyes local MarioFlags = Enums.MarioFlags local MarioHands = Enums.MarioHands local InputFlags = Enums.InputFlags local ModelFlags = Enums.ModelFlags local TerrainType = Enums.TerrainType local SurfaceClass = Enums.SurfaceClass local ParticleFlags = Enums.ParticleFlags local AirStep = Enums.AirStep local GroundStep = Enums.GroundStep export type BodyState = Types.BodyState export type Controller = Types.Controller export type MarioState = Types.MarioState export type Mario = typeof(setmetatable({} :: MarioState, Mario)) export type MarioAction = (Mario) -> boolean export type Flags = Types.Flags export type Class = Mario ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- BINDINGS ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- local actions: { MarioAction } = {} Mario.Animations = Animations Mario.Actions = actions Mario.Sounds = Sounds Mario.Enums = Enums Mario.Types = Types Mario.Util = Util function Mario.RegisterAction(actionType: number, action: MarioAction) if actions[actionType] then warn("Action", Enums.GetName(Action, actionType), "was registered twice!") end actions[actionType] = action end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ANIMATIONS ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- function Mario.IsAnimAtEnd(m: Mario): boolean return m.AnimFrame >= m.AnimFrameCount end function Mario.IsAnimPastEnd(m: Mario): boolean return m.AnimFrame >= m.AnimFrameCount - 2 end function Mario.SetAnimation(m: Mario, anim: Animation): number if anim and typeof(anim) == "Instance" and anim:IsA("Animation") then if m.AnimCurrent == anim then return m.AnimFrame end m.AnimFrameCount = anim:GetAttribute("NumFrames") m.AnimCurrent = anim else warn("Invalid animation provided in SetAnimation:", anim, debug.traceback()) m.AnimFrameCount = 0 m.AnimCurrent = nil end local startFrame: number = anim:GetAttribute("StartFrame") or 0 m.AnimAccelAssist = 0 m.AnimAccel = 0 m.AnimReset = true m.AnimDirty = true m.AnimFrame = startFrame return startFrame end function Mario.SetAnimationWithAccel(m: Mario, anim: Animation, accel: number) if m.AnimCurrent ~= anim then m:SetAnimation(anim) m.AnimAccelAssist = -accel end m.AnimAccel = accel return m.AnimFrame end function Mario.SetAnimToFrame(m: Mario, frame: number) if m.AnimAccel ~= 0 then m.AnimAccelAssist = bit32.lshift(frame, 0x10) + m.AnimAccel m.AnimFrame = bit32.rshift(m.AnimAccelAssist, 0x10) else m.AnimFrame = frame + 1 end m.AnimDirty = true m.AnimSetFrame = m.AnimFrame end function Mario.IsAnimPastFrame(m: Mario, frame: number): boolean local isPastFrame: boolean = false local accel = m.AnimAccel if accel ~= 0 then local assist = m.AnimAccelAssist local accelFrame = bit32.lshift(frame, 0x10) isPastFrame = (assist > accelFrame and accelFrame >= assist - accel) else isPastFrame = (m.AnimFrame == (frame + 1)) end return isPastFrame end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- AUDIO ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- function Mario.PlaySound(m: Mario, sound: Instance?) if not sound then return end assert(sound) if sound:IsA("Sound") then sound:SetAttribute("Play", true) else local rollTable = {} local chances = sound:GetAttributes() for name, chance in pairs(chances) do for i = 1, chance do table.insert(rollTable, name) end end if #rollTable > 0 then local pick = rollTable[math.random(1, #rollTable)] sound = Sounds[pick] if sound then sound:SetAttribute("Play", true) end end end end function Mario.PlaySoundIfNoFlag(m: Mario, sound: Instance?, flags: number) if not m.Flags:Has(flags) and sound then m:PlaySound(sound) m.Flags:Add(flags) end end function Mario.PlayJumpSound(m: Mario) if m.Flags:Has(MarioFlags.MARIO_SOUND_PLAYED) then return end if m.Action() == Action.TRIPLE_JUMP then m:PlaySound(Sounds.MARIO_YAHOO_WAHA_YIPPEE) elseif m.Action() == Action.JUMP_KICK then m:PlaySound(Sounds.MARIO_PUNCH_HOO) else m:PlaySound(Sounds.MARIO_YAH_WAH_HOO) end m.Flags:Add(MarioFlags.MARIO_SOUND_PLAYED) end function Mario.AdjustSoundForSpeed(m: Mario) local _absForwardVel = math.abs(m.ForwardVel) -- TODO: Adjust Moving Speed Pitch end function Mario.PlaySoundAndSpawnParticles(m: Mario, sound: Instance?, wave: number?) local particles: number? if m.TerrainType == TerrainType.WATER then if wave ~= 0 then particles = ParticleFlags.SHALLOW_WATER_SPLASH else particles = ParticleFlags.SHALLOW_WATER_WAVE end else if m.TerrainType == TerrainType.SAND then particles = ParticleFlags.DIRT elseif m.TerrainType == TerrainType.SNOW then particles = ParticleFlags.SNOW end end if particles then m.ParticleFlags:Add(particles) end if sound then local terrainType = Enums.GetName(TerrainType, m.TerrainType) local stepSound = Sounds[sound.Name .. "_" .. terrainType] if stepSound then m:PlaySound(stepSound) else m:PlaySound(sound) end end end function Mario.PlayActionSound(m: Mario, sound: Instance?, wave: number?) if not m.Flags:Has(MarioFlags.ACTION_SOUND_PLAYED) then m:PlaySoundAndSpawnParticles(sound, wave) m.Flags:Add(MarioFlags.ACTION_SOUND_PLAYED) end end function Mario.PlayLandingSound(m: Mario, maybeSound: Instance?) local sound = maybeSound or Sounds.ACTION_TERRAIN_LANDING -- stylua: ignore local landSound = if m.Flags:Has(MarioFlags.METAL_CAP) then Sounds.ACTION_METAL_LANDING else sound m:PlaySoundAndSpawnParticles(landSound, 1) end function Mario.PlayLandingSoundOnce(m: Mario, sound: Instance?) -- stylua: ignore local landSound = if m.Flags:Has(MarioFlags.METAL_CAP) then Sounds.ACTION_METAL_LANDING else sound m:PlayActionSound(landSound, 1) end function Mario.PlayHeavyLandingSound(m: Mario, sound: Instance?) -- stylua: ignore local landSound = if m.Flags:Has(MarioFlags.METAL_CAP) then Sounds.ACTION_METAL_HEAVY_LANDING else sound m:PlaySoundAndSpawnParticles(landSound, 1) end function Mario.PlayHeavyLandingSoundOnce(m: Mario, sound: Instance?) -- stylua: ignore local landSound = if m.Flags:Has(MarioFlags.METAL_CAP) then Sounds.ACTION_METAL_HEAVY_LANDING else sound m:PlayActionSound(landSound, 1) end function Mario.PlayMarioSound(m: Mario, actionSound: Instance, marioSound: Instance?) if marioSound == nil then marioSound = Sounds.MARIO_JUMP end if actionSound == Sounds.ACTION_TERRAIN_JUMP then -- stylua: ignore local sound = if m.Flags:Has(MarioFlags.METAL_CAP) then Sounds.ACTION_METAL_JUMP else actionSound m:PlayActionSound(sound) else m:PlaySoundIfNoFlag(actionSound, MarioFlags.ACTION_SOUND_PLAYED) end if marioSound == Sounds.MARIO_JUMP or marioSound == nil then m:PlayJumpSound() elseif marioSound then m:PlaySoundIfNoFlag(marioSound, MarioFlags.MARIO_SOUND_PLAYED) end end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- ACTION STATE ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- function Mario.SetForwardVel(m: Mario, forwardVel: number) m.ForwardVel = forwardVel m.SlideVelX = Util.Sins(m.FaceAngle.Y) * forwardVel m.SlideVelZ = Util.Coss(m.FaceAngle.Y) * forwardVel m.Velocity = Vector3.new(m.SlideVelX, m.Velocity.Y, m.SlideVelZ) end function Mario.GetFloorClass(m: Mario): number local floor = m.Floor if floor then local hit = floor.Instance if hit and hit:IsA("BasePart") then local physics = hit.CurrentPhysicalProperties local friction = physics.Friction if friction <= 0.025 then return SurfaceClass.VERY_SLIPPERY elseif friction <= 0.5 then return SurfaceClass.SLIPPERY elseif friction >= 0.9 then return SurfaceClass.NOT_SLIPPERY end end end return SurfaceClass.DEFAULT end function Mario.GetTerrainType(m: Mario): number local floor = m.Floor if floor then local material = floor.Material local value = TerrainType.FROM_MATERIAL[material] if value then return value end end return TerrainType.DEFAULT end function Mario.GetFloorType(m: Mario): number -- TODO return 0 end function Mario.FacingDownhill(m: Mario, turnYaw: boolean?): boolean local faceAngleYaw = m.FaceAngle.Y if turnYaw and m.ForwardVel < 0 then faceAngleYaw += 0x8000 end return math.abs(m.FloorAngle - faceAngleYaw) < 0x4000 end function Mario.FloorIsSlippery(m: Mario) local floor = m.Floor if floor then local floorClass = m:GetFloorClass() local deg = 90 if floorClass == SurfaceClass.VERY_SLIPPERY then deg = 10 elseif floorClass == SurfaceClass.SLIPPERY then deg = 20 elseif floorClass == SurfaceClass.NOT_SLIPPERY then deg = 38 end local rad = math.rad(deg) return floor.Normal.Y <= math.cos(rad) end return false end function Mario.FloorIsSlope(m: Mario) local floor = m.Floor if floor then local floorClass = m:GetFloorClass() local deg = 15 if floorClass == SurfaceClass.VERY_SLIPPERY then deg = 5 elseif floorClass == SurfaceClass.SLIPPERY then deg = 10 elseif floorClass == SurfaceClass.NOT_SLIPPERY then deg = 20 end local rad = math.rad(deg) return floor.Normal.Y <= math.cos(rad) end return false end function Mario.FloorIsSteep(m: Mario) local floor = m.Floor if floor and not m:FacingDownhill() then local floorClass = m:GetFloorClass() local deg = 30 if floorClass == SurfaceClass.VERY_SLIPPERY then deg = 15 elseif floorClass == SurfaceClass.SLIPPERY then deg = 20 elseif floorClass == SurfaceClass.NOT_SLIPPERY then deg = 30 end local rad = math.rad(deg) return floor.Normal.Y <= math.cos(rad) end return false end function Mario.FindFloorHeightRelativePolar( m: Mario, angleFromMario: number, distFromMario: number ): (number, RaycastResult?) local y = Util.Sins(m.FaceAngle.Y + angleFromMario) * distFromMario local x = Util.Coss(m.FaceAngle.Y + angleFromMario) * distFromMario local marioPos = m.Position local testPos = marioPos + Vector3.new(y, 100, x) return Util.FindFloor(testPos) end function Mario.FindFloorSlope(m: Mario, yawOffset: number) local x = Util.Sins(m.FaceAngle.Y + yawOffset) * 5 local z = Util.Coss(m.FaceAngle.Y + yawOffset) * 5 local forwardFloorY = Util.FindFloor(m.Position + Vector3.new(x, 100, z)) local backwardFloorY = Util.FindFloor(m.Position + Vector3.new(-x, 100, -z)) local result = 0 if forwardFloorY and backwardFloorY then local forwardYDelta = forwardFloorY - m.Position.Y local backwardYDelta = m.Position.Y - backwardFloorY if forwardYDelta ^ 2 < backwardYDelta ^ 2 then result = Util.Atan2s(5, forwardYDelta) else result = Util.Atan2s(5, backwardYDelta) end end return result end function Mario.SetSteepJumpAction(m: Mario) m.SteepJumpYaw = m.FaceAngle.Y if m.ForwardVel > 0 then local angleTemp = m.FloorAngle + 0x8000 local faceAngleTemp = m.FaceAngle.Y - angleTemp local y = Util.Sins(faceAngleTemp) * m.ForwardVel local x = Util.Coss(faceAngleTemp) * m.ForwardVel * 0.75 m.ForwardVel = math.sqrt(y * y + x * x) m.FaceAngle = Util.SetY(m.FaceAngle, Util.Atan2s(x, y) + angleTemp) end m:SetAction(Action.STEEP_JUMP, 0) end function Mario.SetYVelBasedOnFSpeed(m: Mario, initialVelY: number, multiplier: number) m.Velocity = Util.SetY(m.Velocity, initialVelY + m.ForwardVel * multiplier) if m.SquishTimer ~= 0 or m.QuicksandDepth > 1 then m.Velocity *= Vector3.new(1, 0.5, 1) end end function Mario.SetActionAirborne(m: Mario, action: number, actionArg: number) if m.SquishTimer ~= 0 or m.QuicksandDepth > 1 then if action == Action.DOUBLE_JUMP or action == Action.TWIRLING then action = Action.JUMP end end if action == Action.DOUBLE_JUMP then m:SetYVelBasedOnFSpeed(52, 0.25) m.ForwardVel *= 0.8 elseif action == Action.BACKFLIP then m.AnimReset = true m.ForwardVel = -16 m:SetYVelBasedOnFSpeed(62, 0) elseif action == Action.TRIPLE_JUMP then m:SetYVelBasedOnFSpeed(69, 0) m.ForwardVel *= 0.8 elseif action == Action.FLYING_TRIPLE_JUMP then m:SetYVelBasedOnFSpeed(82, 0) elseif action == Action.WATER_JUMP or action == Action.HOLD_WATER_JUMP then if actionArg == 0 then m:SetYVelBasedOnFSpeed(42, 0) end elseif action == Action.BURNING_JUMP then m.Velocity = Util.SetY(m.Velocity, 31.5) m.ForwardVel = 8 elseif action == Action.RIDING_SHELL_JUMP then m:SetYVelBasedOnFSpeed(42, 0.25) elseif action == Action.JUMP or action == Action.HOLD_JUMP then m.AnimReset = true m:SetYVelBasedOnFSpeed(42, 0.25) m.ForwardVel *= 0.8 elseif action == Action.WALL_KICK_AIR or action == Action.TOP_OF_POLE_JUMP then m:SetYVelBasedOnFSpeed(62, 0) if m.ForwardVel < 24 then m.ForwardVel = 24 end m.WallKickTimer = 0 elseif action == Action.SIDE_FLIP then m:SetYVelBasedOnFSpeed(62, 0) m.ForwardVel = 8 m.FaceAngle = Util.SetY(m.FaceAngle, m.IntendedYaw) elseif action == Action.STEEP_JUMP then m.AnimReset = true m:SetYVelBasedOnFSpeed(42, 0.25) m.FaceAngle = Util.SetX(m.FaceAngle, -0x2000) elseif action == Action.LAVA_BOOST then m.Velocity = Util.SetY(m.Velocity, 84) if actionArg == 0 then m.ForwardVel = 0 end elseif action == Action.DIVE then local forwardVel = m.ForwardVel + 15 if forwardVel > 48 then forwardVel = 48 end m:SetForwardVel(forwardVel) elseif action == Action.LONG_JUMP then m.AnimReset = true m:SetYVelBasedOnFSpeed(30, 0) m.LongJumpIsSlow = if m.ForwardVel > 16 then false else true --! (BLJ's) This properly handles long jumps from getting forward speed with -- too much velocity, but misses backwards longs allowing high negative speeds. m.ForwardVel *= 1.5 if m.ForwardVel > 48 then m.ForwardVel = 48 end elseif action == Action.SLIDE_KICK then m.Velocity = Util.SetY(m.Velocity, 12) if m.ForwardVel < 32 then m.ForwardVel = 32 end elseif action == Action.JUMP_KICK then m.Velocity = Util.SetY(m.Velocity, 20) end m.PeakHeight = m.Position.Y m.Flags:Add(MarioFlags.MOVING_UP_IN_AIR) return action end function Mario.SetActionMoving(m: Mario, action: number, actionArg: number): number local forwardVel = m.ForwardVel local floorClass = m:GetFloorClass() local mag = math.min(m.IntendedMag, 8) if action == Action.WALKING then if floorClass ~= SurfaceClass.VERY_SLIPPERY then if 0.0 <= forwardVel and forwardVel < mag then m.ForwardVel = mag end end m.WalkingPitch = 0 elseif action == Action.HOLD_WALKING then if 0.0 <= forwardVel and forwardVel < mag / 2 then m.ForwardVel = mag / 2 end elseif action == Action.BEGIN_SLIDING then if m:FacingDownhill() then action = Action.BUTT_SLIDE else action = Action.STOMACH_SLIDE end elseif action == Action.HOLD_BEGIN_SLIDING then if m:FacingDownhill() then action = Action.HOLD_BUTT_SLIDE else action = Action.HOLD_STOMACH_SLIDE end end return action end function Mario.SetActionSubmerged(m: Mario, action: number, actionArg: number): number if action == Action.METAL_WATER_JUMP or action == Action.HOLD_METAL_WATER_JUMP then m.Velocity = Util.SetY(m.Velocity, 32) end return action end function Mario.SetActionCutscene(m: Mario, action: number, actionArg: number): number if action == Action.EMERGE_FROM_PIPE then m.Velocity = Util.SetY(m.Velocity, 52) elseif action == Action.FALL_AFTER_STAR_GRAB then m:SetForwardVel(0) elseif action == Action.SPAWN_SPIN_AIRBORNE then m:SetForwardVel(2) elseif action == Action.SPECIAL_EXIT_AIRBORNE or action == Action.SPECIAL_DEATH_EXIT then m.Velocity = Util.SetY(m.Velocity, 64) end return action end function Mario.SetAction(m: Mario, action: number, maybeActionArg: number?): boolean local group = bit32.band(action, ActionGroups.GROUP_MASK) local actionArg = maybeActionArg or 0 if group == ActionGroups.MOVING then action = m:SetActionMoving(action, actionArg) elseif group == ActionGroups.AIRBORNE then action = m:SetActionAirborne(action, actionArg) elseif group == ActionGroups.SUBMERGED then action = m:SetActionSubmerged(action, actionArg) elseif group == ActionGroups.CUTSCENE then action = m:SetActionCutscene(action, actionArg) end m.Flags:Remove(MarioFlags.ACTION_SOUND_PLAYED, MarioFlags.MARIO_SOUND_PLAYED) if not m.Action:Has(ActionFlags.AIR) then m.Flags:Remove(MarioFlags.FALLING_FAR) end m.PrevAction:Copy(m.Action) m.Action:Set(action) m.ActionArg = actionArg m.ActionState = 0 m.ActionTimer = 0 return true end function Mario.SetJumpFromLanding(m: Mario) if m:FloorIsSteep() then m:SetSteepJumpAction() elseif m.DoubleJumpTimer == 0 or m.SquishTimer ~= 0 then m:SetAction(Action.JUMP, 0) else local prev = m.PrevAction() if prev == Action.JUMP_LAND then m:SetAction(Action.DOUBLE_JUMP) elseif prev == Action.FREEFALL_LAND then m:SetAction(Action.DOUBLE_JUMP) elseif prev == Action.SIDE_FLIP_LAND_STOP then m:SetAction(Action.DOUBLE_JUMP) elseif prev == Action.DOUBLE_JUMP_LAND then if m.Flags:Has(MarioFlags.WING_CAP) then m:SetAction(Action.FLYING_TRIPLE_JUMP) elseif m.ForwardVel > 20 then m:SetAction(Action.TRIPLE_JUMP) else m:SetAction(Action.JUMP) end else m:SetAction(Action.JUMP) end end m.DoubleJumpTimer = 0 return true end function Mario.SetJumpingAction(m: Mario, action: number, actionArg: number?) if m:FloorIsSteep() then m:SetSteepJumpAction() else m:SetAction(action, actionArg) end return true end function Mario.HurtAndSetAction(m: Mario, action: number, actionArg: number, hurtCounter: number) m.HurtCounter = hurtCounter m:SetAction(action, actionArg) end function Mario.CheckCommonActionExits(m: Mario) if m.Input:Has(InputFlags.A_PRESSED) then return m:SetAction(Action.JUMP) end if m.Input:Has(InputFlags.OFF_FLOOR) then return m:SetAction(Action.FREEFALL) end if m.Input:Has(InputFlags.NONZERO_ANALOG) then return m:SetAction(Action.WALKING) end if m.Input:Has(InputFlags.ABOVE_SLIDE) then return m:SetAction(Action.BEGIN_SLIDING) end return false end function Mario.UpdatePunchSequence(m: Mario) local endAction, crouchEndAction local animFrame if m.Action:Has(ActionFlags.MOVING) then endAction = Action.WALKING crouchEndAction = Action.CROUCH_SLIDE else endAction = Action.IDLE crouchEndAction = Action.CROUCHING end local actionArg = m.ActionArg if actionArg == 0 or actionArg == 1 then if actionArg == 0 then m:PlaySound(Sounds.MARIO_PUNCH_YAH) end m:SetAnimation(Animations.FIRST_PUNCH) m.ActionArg = m:IsAnimAtEnd() and 2 or 1 if m.AnimFrame >= 2 then m.Flags:Add(MarioFlags.PUNCHING) end elseif actionArg == 2 then m:SetAnimation(Animations.FIRST_PUNCH_FAST) if m.AnimFrame <= 0 then m.Flags:Add(MarioFlags.PUNCHING) end if m.Input:Has(InputFlags.B_PRESSED) then m.ActionArg = 3 end if m:IsAnimAtEnd() then m:SetAction(endAction) end elseif actionArg == 3 or actionArg == 4 then if actionArg == 3 then m:PlaySound(Sounds.MARIO_PUNCH_WAH) end m:SetAnimation(Animations.SECOND_PUNCH) m.ActionArg = m:IsAnimPastEnd() and 5 or 4 if m.AnimFrame > 0 then m.Flags:Add(MarioFlags.PUNCHING) end if m.ActionArg == 5 then m.BodyState.PunchType = 1 m.BodyState.PunchTimer = 4 end elseif actionArg == 5 then m:SetAnimation(Animations.SECOND_PUNCH_FAST) if m.AnimFrame <= 0 then m.Flags:Add(MarioFlags.PUNCHING) end if m.Input:Has(InputFlags.B_PRESSED) then m.ActionArg = 6 end if m:IsAnimAtEnd() then m:SetAction(endAction) end elseif actionArg == 6 then m:PlayActionSound(Sounds.MARIO_PUNCH_HOO, 1) animFrame = m:SetAnimation(Animations.GROUND_KICK) if animFrame == 0 then m.BodyState.PunchType = 2 m.BodyState.PunchTimer = 6 end if animFrame >= 0 and animFrame < 8 then m.Flags:Add(MarioFlags.KICKING) end if m:IsAnimAtEnd() then m:SetAction(endAction) end elseif actionArg == 9 then m:PlayActionSound(Sounds.MARIO_PUNCH_HOO, 1) m:SetAnimation(Animations.BREAKDANCE) animFrame = m.AnimFrame if animFrame >= 2 and animFrame < 8 then m.Flags:Add(MarioFlags.TRIPPING) end if m:IsAnimAtEnd() then m:SetAction(crouchEndAction) end end return false end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- PHYSICS STEP ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- function Mario.BonkReflection(m: Mario, negateSpeed: boolean?) local wall = m.Wall if wall ~= nil then local wallAngle = Util.Atan2s(wall.Normal.Z, wall.Normal.X) m.FaceAngle = Util.SetY(m.FaceAngle, wallAngle - (m.FaceAngle.Y - wallAngle)) m:PlaySound(if m.Flags:Has(MarioFlags.METAL_CAP) then Sounds.ACTION_METAL_BONK else Sounds.ACTION_BONK) else m:PlaySound(Sounds.ACTION_HIT) end if negateSpeed then m:SetForwardVel(-m.ForwardVel) else m.FaceAngle += Vector3int16.new(0, 0x8000, 0) end end function Mario.PushOffSteepFloor(m: Mario, action: number, actionArg: number?) local floorDYaw = m.FloorAngle - m.FaceAngle.Y if floorDYaw > -0x4000 and floorDYaw < 0x4000 then m.ForwardVel = 16 m.FaceAngle = Util.SetY(m.FaceAngle, m.FloorAngle) else m.ForwardVel = -16 m.FaceAngle = Util.SetY(m.FaceAngle, m.FloorAngle + 0x8000) end m:SetAction(action, actionArg) end function Mario.StopAndSetHeightToFloor(m: Mario) m:SetForwardVel(0) m.Velocity *= Vector3.new(1, 0, 1) m.Position = Util.SetY(m.Position, m.FloorHeight) m.GfxAngle = Vector3int16.new(0, m.FaceAngle.Y, 0) end function Mario.StationaryGroundStep(m: Mario): number m:SetForwardVel(0) return m:PerformGroundStep() end function Mario.PerformGroundQuarterStep(m: Mario, nextPos: Vector3): number local lowerPos, _lowerWall = Util.FindWallCollisions(nextPos, 30, 24) nextPos = lowerPos local upperPos, upperWall = Util.FindWallCollisions(nextPos, 60, 50) nextPos = upperPos local floorHeight, floor = Util.FindFloor(nextPos) local ceilHeight, _ceil = Util.FindCeil(nextPos, floorHeight) m.Wall = upperWall if floor == nil then return GroundStep.HIT_WALL_STOP_QSTEPS end if nextPos.Y > floorHeight + 100 then if nextPos.Y + 160 >= ceilHeight then return GroundStep.HIT_WALL_STOP_QSTEPS end m.Floor = floor m.FloorHeight = floorHeight return GroundStep.LEFT_GROUND end if floorHeight + 160 >= ceilHeight then return GroundStep.HIT_WALL_STOP_QSTEPS end m.Floor = floor m.FloorHeight = floorHeight m.Position = Vector3.new(nextPos.X, floorHeight, nextPos.Z) if upperWall then local wallDYaw = Util.SignedShort(Util.Atan2s(upperWall.Normal.Z, upperWall.Normal.X) - m.FaceAngle.Y) if math.abs(wallDYaw) >= 0x2AAA and math.abs(wallDYaw) <= 0x5555 then return GroundStep.NONE end return GroundStep.HIT_WALL_CONTINUE_QSTEPS end return GroundStep.NONE end function Mario.PerformGroundStep(m: Mario): number local floor = m.Floor if not floor then return GroundStep.NONE end local stepResult: number assert(floor) for i = 1, 4 do local intendedX = m.Position.X + floor.Normal.Y * (m.Velocity.X / 4) local intendedZ = m.Position.Z + floor.Normal.Y * (m.Velocity.Z / 4) local intendedY = m.Position.Y local intendedPos = Vector3.new(intendedX, intendedY, intendedZ) stepResult = m:PerformGroundQuarterStep(intendedPos) if stepResult == GroundStep.LEFT_GROUND or stepResult == GroundStep.HIT_WALL_STOP_QSTEPS then break end end m.TerrainType = m:GetTerrainType() m.GfxAngle = Vector3int16.new(0, m.FaceAngle.Y, 0) if stepResult == GroundStep.HIT_WALL_CONTINUE_QSTEPS then stepResult = GroundStep.HIT_WALL end return stepResult end function Mario.CheckLedgeGrab(m: Mario, wall: RaycastResult, intendedPos: Vector3, nextPos: Vector3): boolean if m.Velocity.Y > 0 then return false end local dispX = nextPos.X - intendedPos.X local dispZ = nextPos.Z - intendedPos.Z if dispX * m.Velocity.X + dispZ * m.Velocity.Z > 0 then return false end local ledgeX = nextPos.X - (wall.Normal.X * 60) local ledgeZ = nextPos.Z - (wall.Normal.Z * 60) local ledgePos = Vector3.new(ledgeX, nextPos.Y + 160, ledgeZ) local ledgeY, ledgeFloor = Util.FindFloor(ledgePos) if ledgeY - nextPos.Y < 100 then return false end if ledgeFloor then ledgePos = ledgeFloor.Position m.Position = ledgePos m.Floor = ledgeFloor m.FloorHeight = ledgeY m.FloorAngle = Util.Atan2s(ledgeFloor.Normal.Z, ledgeFloor.Normal.X) m.FaceAngle *= Vector3int16.new(0, 1, 1) m.FaceAngle = Util.SetY(m.FaceAngle, Util.Atan2s(wall.Normal.Z, wall.Normal.X) + 0x8000) end return ledgeFloor ~= nil end function Mario.PerformAirQuarterStep(m: Mario, intendedPos: Vector3, stepArg: number) local nextPos = intendedPos local upperPos, upperWall = Util.FindWallCollisions(nextPos, 150, 50) nextPos = upperPos local lowerPos, lowerWall = Util.FindWallCollisions(nextPos, 30, 50) nextPos = lowerPos local floorHeight, floor = Util.FindFloor(nextPos) local ceilHeight = Util.FindCeil(nextPos, floorHeight) m.Wall = nil if floor == nil then if nextPos.Y <= m.FloorHeight then m.Position = Util.SetY(m.Position, m.FloorHeight) return AirStep.LANDED end m.Position = Util.SetY(m.Position, nextPos.Y) return AirStep.HIT_WALL end if nextPos.Y <= floorHeight then if ceilHeight - floorHeight > 160 then m.Floor = floor m.FloorHeight = floorHeight m.Position = Vector3.new(nextPos.X, m.Position.Y, nextPos.Z) end m.Position = Util.SetY(m.Position, floorHeight) return AirStep.LANDED end if nextPos.Y + 160 > ceilHeight then if m.Velocity.Y > 0 then m.Velocity = Util.SetY(m.Velocity, 0) return AirStep.NONE end if nextPos.Y <= m.FloorHeight then m.Position = Util.SetY(m.Position, floorHeight) return AirStep.LANDED end m.Position = Util.SetY(m.Position, nextPos.Y) return AirStep.HIT_WALL end if bit32.btest(stepArg, AirStep.CHECK_LEDGE_GRAB) and upperWall == nil and lowerWall ~= nil then if m:CheckLedgeGrab(lowerWall, intendedPos, nextPos) then return AirStep.GRABBED_LEDGE end m.Floor = floor m.Position = nextPos m.FloorHeight = floorHeight return AirStep.NONE end m.Floor = floor m.Position = nextPos m.FloorHeight = floorHeight if upperWall or lowerWall then local wall = assert(upperWall or lowerWall) local wallDYaw = Util.SignedShort(Util.Atan2s(wall.Normal.Z, wall.Normal.X) - m.FaceAngle.Y) m.Wall = wall if math.abs(wallDYaw) > 0x6000 then return AirStep.HIT_WALL end end return AirStep.NONE end function Mario.ApplyTwirlGravity(m: Mario) local heaviness = 1 if m.AngleVel.Y > 1024 then heaviness = 1024 / m.AngleVel.Y end local terminalVelocity = -75 * heaviness m.Velocity -= Vector3.new(0, 4 * heaviness, 0) if m.Velocity.Y < terminalVelocity then m.Velocity = Util.SetY(m.Velocity, terminalVelocity) end end function Mario.ShouldStrengthenGravityForJumpAscent(m: Mario): boolean if not m.Flags:Has(MarioFlags.MOVING_UP_IN_AIR) then return false end if m.Action:Has(ActionFlags.INTANGIBLE, ActionFlags.INVULNERABLE) then return false end if not m.Input:Has(InputFlags.A_DOWN) and m.Velocity.Y > 20 then return m.Action:Has(ActionFlags.CONTROL_JUMP_HEIGHT) end return false end function Mario.ApplyGravity(m: Mario) local action = m.Action() if action == Action.TWIRLING and m.Velocity.Y < 0 then m:ApplyTwirlGravity() elseif action == Action.SHOT_FROM_CANNON then m.Velocity -= Vector3.yAxis if m.Velocity.Y < -75 then m.Velocity = Util.SetY(m.Velocity, -75) end elseif action == Action.LONG_JUMP or action == Action.SLIDE_KICK or action == Action.BBH_ENTER_SPIN then m.Velocity -= (Vector3.yAxis * 2) if m.Velocity.Y < -75 then m.Velocity = Util.SetY(m.Velocity, -75) end elseif action == Action.LAVA_BOOST or action == Action.FALL_AFTER_STAR_GRAB then m.Velocity -= (Vector3.yAxis * 3.2) if m.Velocity.Y < -65 then m.Velocity = Util.SetY(m.Velocity, -65) end elseif m:ShouldStrengthenGravityForJumpAscent() then m.Velocity *= Vector3.new(1, 0.25, 1) elseif m.Action:Has(ActionFlags.METAL_WATER) then m.Velocity -= (Vector3.yAxis * 1.6) if m.Velocity.Y < -16 then m.Velocity = Util.SetY(m.Velocity, -16) end elseif m.Flags:Has(MarioFlags.WING_CAP) and m.Velocity.Y < 0 and m.Input:Has(InputFlags.A_DOWN) then m.BodyState.WingFlutter = true m.Velocity -= (Vector3.yAxis * 2) if m.Velocity.Y < -37.5 then m.Velocity += (Vector3.yAxis * 4) if m.Velocity.Y > -37.5 then m.Velocity = Util.SetY(m.Velocity, -37.5) end end else m.Velocity -= (Vector3.yAxis * 4) if m.Velocity.Y < -75 then m.Velocity = Util.SetY(m.Velocity, -75) end end end function Mario.PerformAirStep(m: Mario, maybeStepArg: number?) local stepArg = maybeStepArg or 0 local stepResult = AirStep.NONE m.Wall = nil for i = 1, 4 do local intendedPos = m.Position + (m.Velocity / 4) local result = m:PerformAirQuarterStep(intendedPos, stepArg) if result ~= AirStep.NONE then stepResult = result end if result == AirStep.LANDED or result == AirStep.GRABBED_LEDGE or result == AirStep.GRABBED_CEILING or result == AirStep.HIT_LAVA_WALL then break end end if m.Velocity.Y >= 0 then m.PeakHeight = m.Position.Y end m.TerrainType = m:GetTerrainType() if m.Action() ~= Action.FLYING then m:ApplyGravity() end m.GfxAngle = Vector3int16.new(0, m.FaceAngle.Y, 0) return stepResult end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- UPDATE ROUTINES ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- function Mario.UpdateButtonInputs(m: Mario) if m.Controller.ButtonPressed:Has(Buttons.A_BUTTON) then m.Input:Add(InputFlags.A_PRESSED) end if m.Controller.ButtonDown:Has(Buttons.A_BUTTON) then m.Input:Add(InputFlags.A_DOWN) end if m.SquishTimer == 0 then if m.Controller.ButtonPressed:Has(Buttons.B_BUTTON) then m.Input:Add(InputFlags.B_PRESSED) end if m.Controller.ButtonDown:Has(Buttons.Z_TRIG) then m.Input:Add(InputFlags.Z_DOWN) end if m.Controller.ButtonPressed:Has(Buttons.Z_TRIG) then m.Input:Add(InputFlags.Z_PRESSED) end end if m.Input:Has(InputFlags.A_PRESSED) then m.FramesSinceA = 0 elseif m.FramesSinceA < 255 then m.FramesSinceA += 1 end if m.Input:Has(InputFlags.B_PRESSED) then m.FramesSinceB = 0 elseif m.FramesSinceB < 255 then m.FramesSinceB += 1 end end function Mario.UpdateJoystickInputs(m: Mario) local controller = m.Controller local mag = ((controller.StickMag / 64) * (controller.StickMag / 64)) * 64 if m.SquishTimer == 0 then m.IntendedMag = mag / 2 else m.IntendedMag = mag / 8 end if m.IntendedMag > 0 then local camera = workspace.CurrentCamera local lookVector = camera.CFrame.LookVector local cameraYaw = Util.Atan2s(-lookVector.Z, -lookVector.X) m.IntendedYaw = Util.SignedShort(Util.Atan2s(-controller.StickY, controller.StickX) + cameraYaw) m.Input:Add(InputFlags.NONZERO_ANALOG) else m.IntendedYaw = m.FaceAngle.Y end end function Mario.UpdateGeometryInputs(m: Mario) local floorHeight, floor = Util.FindFloor(m.Position) local ceilHeight, ceil = Util.FindCeil(m.Position, m.FloorHeight) m.FloorHeight = floorHeight m.CeilHeight = ceilHeight m.Floor = floor m.Ceil = ceil if floor then m.FloorAngle = Util.Atan2s(floor.Normal.Z, floor.Normal.X) m.TerrainType = m:GetTerrainType() if m:FloorIsSlippery() then m.Input:Add(InputFlags.ABOVE_SLIDE) end if ceil then local ceilToFloorDist = m.CeilHeight - m.FloorHeight if 0 < ceilToFloorDist and ceilToFloorDist < 150 then m.Input:Add(InputFlags.SQUISHED) end end if m.Position.Y > m.FloorHeight + 100 then m.Input:Add(InputFlags.OFF_FLOOR) end if m.Position.Y < m.WaterLevel - 10 then m.Input:Add(InputFlags.IN_WATER) end end end function Mario.UpdateInputs(m: Mario) m.ParticleFlags:Clear() m.Flags:Band(0xFFFFFF) m.Input:Clear() m:UpdateButtonInputs() m:UpdateJoystickInputs() m:UpdateGeometryInputs() if not m.Input:Has(InputFlags.NONZERO_ANALOG, InputFlags.A_PRESSED) then m.Input:Add(InputFlags.NO_MOVEMENT) end if m.WallKickTimer > 0 then m.WallKickTimer -= 1 end if m.DoubleJumpTimer > 0 then m.DoubleJumpTimer -= 1 end end function Mario.ResetBodyState(m: Mario) local bodyState = m.BodyState bodyState.CapState:Set(MarioCap.DEFAULT_CAP_OFF) bodyState.EyeState = MarioEyes.BLINK bodyState.HandState:Set(MarioHands.FISTS) bodyState.ModelState:Clear() bodyState.WingFlutter = false m.Flags:Remove(MarioFlags.METAL_SHOCK) end function Mario.UpdateCaps(m: Mario): Flags local flags = m.Flags if m.CapTimer > 0 then if m.CapTimer <= 60 then m.CapTimer -= 1 end if m.CapTimer == 0 then m.Flags:Remove(MarioFlags.SPECIAL_CAPS) if not m.Flags:Has(MarioFlags.CAPS) then m.Flags:Remove(MarioFlags.CAP_ON_HEAD) end end end return flags end function Mario.UpdateModel(m: Mario) local modelState = Flags.new() local bodyState = m.BodyState local flags = m:UpdateCaps() if flags:Has(MarioFlags.VANISH_CAP) then modelState:Add(ModelFlags.NOISE_ALPHA) end if flags:Has(MarioFlags.METAL_CAP, MarioFlags.METAL_SHOCK) then modelState:Add(ModelFlags.METAL) end if m.InvincTimer >= 3 and bit32.band(Util.GlobalTimer, 1) > 0 then modelState:Add(ModelFlags.INVISIBLE) end if flags:Has(MarioFlags.CAP_IN_HAND) then if flags:Has(MarioFlags.WING_CAP) then bodyState.HandState:Set(MarioHands.HOLDING_WING_CAP) else bodyState.HandState:Set(MarioHands.HOLDING_CAP) end end if flags:Has(MarioFlags.CAP_ON_HEAD) then if flags:Has(MarioFlags.WING_CAP) then bodyState.CapState:Set(MarioCap.WING_CAP_ON) else bodyState.CapState:Set(MarioCap.DEFAULT_CAP_ON) end end if m.Action:Has(ActionFlags.SHORT_HITBOX) then m.HitboxHeight = 100 else m.HitboxHeight = 160 end end function Mario.CheckKickOrPunchWall(m: Mario) if m.Flags:Has(MarioFlags.PUNCHING, MarioFlags.KICKING, MarioFlags.TRIPPING) then -- stylua: ignore local range = Vector3.new( Util.Sins(m.FaceAngle.Y), 0, Util.Coss(m.FaceAngle.Y) ) local detector = m.Position + (range * 50) local _disp, wall = Util.FindWallCollisions(detector, 80, 5) if wall then if m.Action() ~= Action.MOVE_PUNCHING or m.ForwardVel >= 0 then if m.Action() == Action.PUNCHING then m.Action:Set(Action.MOVE_PUNCHING) end m:SetForwardVel(-48) m:PlaySound(Sounds.ACTION_HIT) m.ParticleFlags:Add(ParticleFlags.TRIANGLE) elseif m.Action:Has(ActionFlags.AIR) then m:SetForwardVel(-16) m:PlaySound(Sounds.ACTION_HIT) m.ParticleFlags:Add(ParticleFlags.TRIANGLE) end end end end function Mario.ProcessInteractions(m: Mario) if m.InvincTimer > 0 then m.InvincTimer -= 1 end m:CheckKickOrPunchWall() m.Flags:Remove(MarioFlags.PUNCHING, MarioFlags.KICKING, MarioFlags.TRIPPING) end function Mario.HandleSpecialFloors(m: Mario) local floor = m.Floor if floor and not m.Action:Has(ActionFlags.AIR, ActionFlags.SWIMMING) then if floor.Material == Enum.Material.CrackedLava then if not m.Flags:Has(MarioFlags.METAL_CAP) then m.HurtCounter += m.Flags:Has(MarioFlags.CAP_ON_HEAD) and 12 or 18 end m:SetAction(Action.LAVA_BOOST) end end end function Mario.SetWaterPlungeAction(m: Mario) m.ForwardVel /= 4 m.Velocity *= Vector3.new(1, 0.5, 1) -- This behavior sucks, feel free to enable if you want. -- m.Position = Util.SetY(m.Position, m.WaterLevel - 100) m.FaceAngle *= Vector3int16.new(1, 1, 0) m.AngleVel *= 0 if not m.Action:Has(ActionFlags.DIVING) then m.FaceAngle *= Vector3int16.new(0, 1, 1) end return m:SetAction(Action.WATER_PLUNGE) end function Mario.PlayFarFallSound(m: Mario) if m.Flags:Has(MarioFlags.FALLING_FAR) then return end local action = m.Action if action() == Action.TWIRLING then return end if action() == Action.FLYING then return end if action:Has(ActionFlags.INVULNERABLE) then return end if m.PeakHeight - m.Position.Y > 1150 then m:PlaySound(Sounds.MARIO_WAAAOOOW) m.Flags:Add(MarioFlags.FALLING_FAR) end end function Mario.ExecuteAction(m: Mario): number if m.Action() == 0 then return 0 end m.AnimFrame += 1 m.AnimFrame %= (m.AnimFrameCount + 1) if m.AnimAccel > 0 then m.AnimAccelAssist += m.AnimAccel m.AnimAccelAssist %= bit32.lshift(m.AnimFrameCount + 1, 0x10) end if m.SquishTimer > 0 then m.SquishTimer -= 1 end m.GfxAngle *= 0 m.AnimDirty = true m.ThrowMatrix = nil m.AnimSkipInterp = math.max(0, m.AnimSkipInterp - 1) m:ResetBodyState() m:UpdateInputs() m:HandleSpecialFloors() m:ProcessInteractions() if m.Floor == nil then return 0 end while m.Action() > 0 do local id = m.Action() local action = actions[id] if action then local group = bit32.band(id, ActionGroups.GROUP_MASK) local cancel if group ~= ActionGroups.SUBMERGED and m.Position.Y < m.WaterLevel - 100 then cancel = m:SetWaterPlungeAction() else if group == ActionGroups.AIRBORNE then m:PlayFarFallSound() elseif group == ActionGroups.SUBMERGED then if m.Position.Y > m.WaterLevel - 80 then if m.WaterLevel - 80 > m.FloorHeight then m.Position = Util.SetY(m.Position, m.WaterLevel - 80) else m.AngleVel *= 0 cancel = m:SetAction(Action.WALKING) end end m.QuicksandDepth = 0 m.BodyState.HeadAngle *= Vector3int16.new(1, 0, 0) end if cancel == nil then cancel = action(m) end end if not cancel then if m.Input:Has(InputFlags.IN_WATER) then if group == ActionGroups.MOVING then m.ParticleFlags:Add(ParticleFlags.WAVE_TRAIL) m.ParticleFlags:Remove(ParticleFlags.DUST) elseif group == ActionGroups.STATIONARY then m.ParticleFlags:Add(ParticleFlags.IDLE_WATER_WAVE) end end break end else local name = Enums.GetName(Action, id) if name then warn("Unhandled Action:", name) else warn("UNKNOWN ACTION:", id) end m.Action:Set(Action.IDLE) break end end -- m:SinkInQuicksand() -- m:SquishModel() -- m:UpdateHealth() m:UpdateModel() return m.ParticleFlags() end ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- INITIALIZATION ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- function Mario.new(): Mario local bodyState: BodyState = { Action = 0, CapState = Flags.new(), EyeState = 0, HandState = Flags.new(), WingFlutter = false, ModelState = Flags.new(), GrabPos = 0, PunchType = 0, PunchTimer = 0, HeadAngle = Vector3int16.new(), TorsoAngle = Vector3int16.new(), HeldObjLastPos = Vector3.zero, } local controller: Controller = { RawStickX = 0, RawStickY = 0, StickX = 0, StickY = 0, StickMag = 0, ButtonDown = Flags.new(), ButtonPressed = Flags.new(), } local state: MarioState = { Input = Flags.new(), Flags = Flags.new(MarioFlags.NORMAL_CAP, MarioFlags.CAP_ON_HEAD), Action = Flags.new(Action.SPAWN_SPIN_AIRBORNE), PrevAction = Flags.new(), ParticleFlags = Flags.new(), HitboxHeight = 0, TerrainType = 0, ActionState = 0, ActionTimer = 0, ActionArg = 0, IntendedMag = 0, IntendedYaw = 0, InvincTimer = 0, FramesSinceA = 255, FramesSinceB = 255, WallKickTimer = 0, DoubleJumpTimer = 0, FaceAngle = Vector3int16.new(), AngleVel = Vector3int16.new(), ThrowMatrix = CFrame.identity, GfxAngle = Vector3int16.new(), GfxPos = Vector3.zero, SlideYaw = 0, TwirlYaw = 0, Position = Vector3.yAxis * 500, Velocity = Vector3.zero, ForwardVel = 0, SlideVelX = 0, SlideVelZ = 0, CeilHeight = 0, FloorHeight = 0, FloorAngle = 0, WaterLevel = 0, Health = 0x880, HurtCounter = 0, HealCounter = 0, SquishTimer = 0, CapTimer = 0, BurnTimer = 0, PeakHeight = 0, SteepJumpYaw = 0, WalkingPitch = 0, QuicksandDepth = 0, LongJumpIsSlow = false, BodyState = bodyState, Controller = controller, AnimAccel = 0, AnimFrame = -1, AnimSetFrame = -1, AnimDirty = false, AnimReset = false, AnimFrameCount = 0, AnimAccelAssist = 0, AnimSkipInterp = 0, } return setmetatable(state, Mario) end return Mario