--[[ -- Spectator Mode Addon for SRB2 -- Introduces a feature allowing players to view the game as a free-floating camera -- or by tracking specific objects. -- 'Freecamera' mode: Viewpoint is detached, allowing free navigation of the level. -- 'Tracking object' mode: Follows a chosen entity automatically. -- Toggles: Camera can switch between players and objects for focused perspective. --]] if sirexer_spectator_mode then print("\x85".."ERROR".."\x80"..": Spectator Mode is already added in the game!") return else rawset(_G, "sirexer_spectator_mode", true) end local sm_addon = { name = "Spectator Camera", version = 1, subversion = 1, author = "Sirexer", } if not addoninfo then rawset(_G, "addoninfo", {}) end table.insert(addoninfo, sm_addon) -- Allocate slots for the camera object and the "tagging" projectile freeslot("MT_SPECCAMERA", "MT_CATCHIN4K") -- Definition of the Camera Object mobjinfo[MT_SPECCAMERA] = { doomednum = -1, spawnstate = S_INVISIBLE, spawnhealth = 1000, reactiontime = 5, radius = 16*FU, height = 32*FU, flags = MF_NOGRAVITY|MF_NOCLIP|MF_NOCLIPHEIGHT|MF_ENEMY -- MF_ENEMY used for some interaction logic later } -- Definition of the "Tagging" Projectile (fires to select an object to watch) mobjinfo[MT_CATCHIN4K] = { doomednum = -1, spawnstate = S_INVISIBLE, spawnhealth = 1000, reactiontime = 5, radius = 32*FU, height = 32*FU, speed = 128*FU, flags = MF_NOGRAVITY|MF_SPECIAL } local FixedMul = FixedMul local FixedDiv = FixedDiv local FixedHypot = FixedHypot local sin, cos = sin, cos local abs = abs -- CVAR to allow or disallow noclip for the camera (Server setting) local sm_allownoclip = CV_RegisterVar({ name = "sm_allownoclip", defaultvalue = 1, flags = CV_NETVAR|CV_SAVE, PossibleValue = {No = 0, Yes = 1}} ) local sm_chase = nil -- Helper: Check if objects exist and are valid local IsValid = function(args, ...) if args ~= #{...} then return false end for k, v in ipairs({...}) do if not v or v.valid == false then return false end end return true end -- Helper: Find a player by node ID or name local findplayer = function(p) if tonumber(p) == 0 or p == "00" then return server end local node = tonumber(p) if node ~= nil and node >= 0 and node < 32 then for player in players.iterate do if #player == node then return player end end end for player in players.iterate do if string.lower(p) == string.lower(player.name) then return player end if string.find(string.lower(player.name), string.lower(p)) then return player end end return nil end local DAMP = FU*30/32 local STOP_EPS = FU/256 -- Helper: Apply friction/damping to a value local function Damp(value, damp, stop_eps) value = FixedMul(value, damp) if abs(value) < stop_eps then return 0 end return value end -- Helper: Spawn a missile (used for the object selector projectile) local P_SpawnMissileAngle = function(source, speed, h_angle, v_angle, type) local x, y, z = source.x, source.y, source.z-(8*source.scale) local mo = P_SpawnMobj(x, y, z, type) P_Thrust(mo, h_angle, cos(v_angle)*speed) mo.momz = sin(v_angle)*speed mo.source = source return mo end -- Initialize the Camera for a player local SpawnCamera = function(player, object, x, y, z) local pmo = player.realmo if not object then object = pmo end x, y, z = x or object.x, y or object.y, z or object.z -- Initialize the camera data structure player.speccam = { mo = P_SpawnMobj(x, y, z, MT_SPECCAMERA), type = "freecamera", movexy = TICRATE, -- Timers for movement smoothing/stopping movez = TICRATE, fov = 0, speed = 100, noclip = false, gravity = false, cinema = false, -- Smooth camera movement invert = false, follow = true, -- For object mode: actually follow position? blockplayer = true, -- Stop player input from moving the real character t_angle = true, -- Track angle of target chase = true, -- Chase cam vs fixed view hud = true, -- Button state trackers to prevent rapid toggling c1_p = false, c2_p = false, c3_p = false, w_p = false, tf_p = false } player.awayviewaiming = 0 player.speccam.mo.target = player.realmo if player == consoleplayer then camera.chase = true sm_chase = camera.chase end end -- Clean up camera and restore normal view local RemoveCamera = function(player) player.viewrollangle = 0 player.realmo.angle = player.drawangle if IsValid(2, player, player.speccam.mo) then P_RemoveMobj(player.speccam.mo) end if player == consoleplayer then if hud.enabled("textspectator") == false then hud.enable("textspectator") end camera.chase = sm_chase end player.speccam = nil end --========================================================-- -- Freecamera movement thinker --========================================================-- local function moveCamera(player) local cam = player.speccam local cmd = player.cmd local pmo = player.realmo local cmo = cam.mo local yaw = cmo.angle local pitch = player.awayviewaiming local roll = player.viewrollangle local basespeed = cam.speed local speed_div = 4 -- Boost speed with Custom1 button if (cmd.buttons & BT_CUSTOM1) then speed_div = 1 end -- Slow down significantly in glide mode for precision if cam.glide then speed_div = 64 end local fixedspeed = FixedMul(FRACUNIT, FixedDiv(basespeed * FRACUNIT, 100*FRACUNIT)) local moveforce = fixedspeed * 100 / speed_div local sinyaw, cosyaw = sin(yaw), cos(yaw) local sinpitch, cospitch = sin(pitch), cos(pitch) local sinroll, cosroll = sin(roll), cos(roll) -- Handle Forward/Backward Movement if cmd.forwardmove ~= 0 then local input = cmd.forwardmove * FRACUNIT local scale = FixedMul(FRACUNIT, FixedDiv(input, 50*FRACUNIT)) local finalforce = FixedMul(moveforce, scale) local z = sinpitch local xy = cospitch -- Calculate momentum vectors based on camera direction cmo.momx = $ + FixedMul(FixedMul(xy, cosyaw), finalforce) cmo.momy = $ + FixedMul(FixedMul(xy, sinyaw), finalforce) cmo.momz = $ + FixedMul(z, finalforce) end -- Handle Strafe Movement if cmd.sidemove ~= 0 and not (cmd.buttons & BT_FIRENORMAL) then local input = cmd.sidemove * FRACUNIT local scale = FixedMul(FRACUNIT, FixedDiv(input, 50*FRACUNIT)) local finalforce = FixedMul(moveforce, scale) -- Math to move perpendicular to view direction (considering roll) local x = FixedMul(sinyaw, cosroll) + FixedMul(cosyaw, FixedMul(sinpitch, sinroll)) local y = -FixedMul(cosyaw, cosroll) + FixedMul(sinyaw, FixedMul(sinpitch, sinroll)) local z = FixedMul(cospitch, sinroll) cmo.momx = $ + FixedMul(x, finalforce) cmo.momy = $ + FixedMul(y, finalforce) cmo.momz = $ - FixedMul(z, finalforce) end -- Vertical Movement (Up/Down relative to camera up-vector) if P_MobjFlip(pmo) ~= 1 and (player.pflags & PF_FLIPCAM) then moveforce = -$ end local z = FixedMul(cosroll, cospitch) local x = -FixedMul(cosyaw, FixedMul(sinpitch, cosroll)) + FixedMul(sinyaw, sinroll) local y = -FixedMul(sinyaw, FixedMul(sinpitch, cosroll)) - FixedMul(cosyaw, sinroll) if (cmd.buttons & BT_JUMP) then cmo.momx = $ + FixedMul(x, moveforce) cmo.momy = $ + FixedMul(y, moveforce) cmo.momz = $ + FixedMul(z, moveforce) end if (cmd.buttons & BT_SPIN) then cmo.momx = $ - FixedMul(x, moveforce) cmo.momy = $ - FixedMul(y, moveforce) cmo.momz = $ - FixedMul(z, moveforce) end -- Gravity logic: Prevent flying up if gravity is on, unless gliding if cam.gravity and not cam.glide then if P_MobjFlip(cmo) == 1 and cmo.momz < 0 then elseif cmo.momz > 0 then cmo.momz = 0 end end end -- Calculates camera roll based on input or cinema smoothing local function handleRoll(player, currentroll) local cam = player.speccam local cmd = player.cmd local roll = currentroll or 0 if cam.cinema then -- Smooth roll using momentum (c_mz) cam.c_mz = cam.c_mz or 0 if (cmd.buttons & BT_FIRENORMAL) then cam.c_mz = $ + (cmd.sidemove * FU) / 2 end local maxspeed = 512*FU if abs(cam.c_mz) > maxspeed then cam.c_mz = (cam.c_mz > 0 and maxspeed or -maxspeed) elseif abs(cmd.sidemove) <= 5 then cam.c_mz = Damp(cam.c_mz, FU*30/32, 32) end roll = $ + cam.c_mz else -- Direct roll control when holding FireNormal if (cmd.buttons & BT_FIRENORMAL) and abs(cmd.sidemove) >= 5 then roll = $ + cmd.sidemove * 8 * FU end end return roll end -- Smoothes mouse input for cinematic feel local function cinemaSmoothing(cam, mousex, mousey) if not cam.cinema then cam.c_mx, cam.c_my = 0, 0 return mousex, mousey end cam.c_mx, cam.c_my = cam.c_mx or 0, cam.c_my or 0 if abs(mousex) >= 16 then cam.c_mx = cam.c_mx + mousex/16 else cam.c_mx = Damp(cam.c_mx, FU*30/32, 16) end if abs(mousey) >= 16 then cam.c_my = cam.c_my + mousey/16 else cam.c_my = Damp(cam.c_my, FU*30/32, 16) end return cam.c_mx, cam.c_my end -- Converts current camera Euler angles to a 3-vector axis system (Forward, Right, Up) local function anglesToAxis(cmo, player) local axis = {} local fyaw, fpitch = cmo.angle, -player.awayviewaiming local cy, sy = cos(fyaw), sin(fyaw) local cp, sp = cos(fpitch), sin(fpitch) -- Forward vector axis.fx, axis.fy, axis.fz = FixedMul(cp, cy), FixedMul(cp, sy), sp -- Calculate Right and Up vectors local wx, wy, wz = 0, 0, FU axis.rx = FixedMul(axis.fy, wz) - FixedMul(axis.fz, wy) axis.ry = FixedMul(axis.fz, wx) - FixedMul(axis.fx, wz) axis.rz = FixedMul(axis.fx, wy) - FixedMul(axis.fy, wx) local len = FixedHypot(axis.rx, FixedHypot(axis.ry, axis.rz)) axis.rx, axis.ry, axis.rz = FixedDiv(axis.rx, len), FixedDiv(axis.ry, len), FixedDiv(axis.rz, len) axis.ux = FixedMul(axis.ry, axis.fz) - FixedMul(axis.rz, axis.fy) axis.uy = FixedMul(axis.rz, axis.fx) - FixedMul(axis.rx, axis.fz) axis.uz = FixedMul(axis.rx, axis.fy) - FixedMul(axis.ry, axis.fx) -- Apply Roll local roll = player.viewrollangle local s, c = sin(roll), cos(roll) local rx, ry, rz = axis.rx, axis.ry, axis.rz local ux, uy, uz = axis.ux, axis.uy, axis.uz axis.rx = FixedMul(rx, c) + FixedMul(ux, s) axis.ry = FixedMul(ry, c) + FixedMul(uy, s) axis.rz = FixedMul(rz, c) + FixedMul(uz, s) axis.ux = FixedMul(ux, c) - FixedMul(rx, s) axis.uy = FixedMul(uy, c) - FixedMul(ry, s) axis.uz = FixedMul(uz, c) - FixedMul(rz, s) return axis end -- Re-calculates roll angle from the axis system local function computeRoll(axis, flipped) local dot = FixedMul(FU, axis.fz) local wz = flipped and -FU or FU local px = -FixedMul(axis.fx, dot) local py = -FixedMul(axis.fy, dot) local pz = FU - FixedMul(axis.fz, dot) local len = FixedHypot(px, FixedHypot(py, pz)) if not len then return 0 end px, py, pz = FixedDiv(px, len), FixedDiv(py, len), FixedDiv(pz, len) local sinv = FixedMul(axis.rx, px) + FixedMul(axis.ry, py) + FixedMul(axis.rz, pz) local cosv = FixedMul(axis.ux, px) + FixedMul(axis.uy, py) + FixedMul(axis.uz, pz) return R_PointToAngle2(0, 0, cosv, sinv) end -- 6DOF (Six Degrees of Freedom) Rotation Logic -- Allows looking in any direction without "Gimbal Lock" local function rotateCamera6DOF(player) local cam = player.speccam local axis = cam.axis local cmd = player.cmd local pmo = player.mo local cmo = cam.mo local flipped = P_MobjFlip(pmo) == -1 local mousex = cmd.angleturn local mousey = cmd.aiming if abs(mousex) <= 8 then mousex = 0 end if abs(mousey) <= 8 then mousey = 0 end mousex, mousey = cinemaSmoothing(cam, mousex, mousey) local rolldelta = handleRoll(player) if not mousex and not mousey and not rolldelta then return end -- Apply Pitch (Y-axis rotation) if mousey ~= 0 then local angle = -mousey * FU local s, c = sin(angle), cos(angle) local fx, fy, fz = axis.fx, axis.fy, axis.fz local ux, uy, uz = axis.ux, axis.uy, axis.uz axis.fx = FixedMul(fx, c) + FixedMul(ux, s) axis.fy = FixedMul(fy, c) + FixedMul(uy, s) axis.fz = FixedMul(fz, c) + FixedMul(uz, s) axis.ux = FixedMul(ux, c) - FixedMul(fx, s) axis.uy = FixedMul(uy, c) - FixedMul(fy, s) axis.uz = FixedMul(uz, c) - FixedMul(fz, s) end -- Apply Yaw (X-axis rotation) if mousex ~= 0 then local angle = -mousex * FU local s, c = sin(angle), cos(angle) local fx, fy, fz = axis.fx, axis.fy, axis.fz local rx, ry, rz = axis.rx, axis.ry, axis.rz axis.fx = FixedMul(fx, c) + FixedMul(rx, s) axis.fy = FixedMul(fy, c) + FixedMul(ry, s) axis.fz = FixedMul(fz, c) + FixedMul(rz, s) axis.rx = FixedMul(rx, c) - FixedMul(fx, s) axis.ry = FixedMul(ry, c) - FixedMul(fy, s) axis.rz = FixedMul(rz, c) - FixedMul(fz, s) end -- Apply Roll (Z-axis rotation) if rolldelta ~= 0 then local s, c = sin(rolldelta), cos(rolldelta) local rx, ry, rz = axis.rx, axis.ry, axis.rz local ux, uy, uz = axis.ux, axis.uy, axis.uz axis.rx = FixedMul(rx, c) + FixedMul(ux, s) axis.ry = FixedMul(ry, c) + FixedMul(uy, s) axis.rz = FixedMul(rz, c) + FixedMul(uz, s) axis.ux = FixedMul(ux, c) - FixedMul(rx, s) axis.uy = FixedMul(uy, c) - FixedMul(ry, s) axis.uz = FixedMul(uz, c) - FixedMul(rz, s) end -- Normalize Forward vector local len = FixedHypot(axis.fx, FixedHypot(axis.fy, axis.fz)) if len then axis.fx = FixedDiv(axis.fx, len) axis.fy = FixedDiv(axis.fy, len) axis.fz = FixedDiv(axis.fz, len) end -- Reconstruct Right/Up vectors via Cross Product to maintain orthogonality axis.rx = FixedMul(axis.fy, axis.uz) - FixedMul(axis.fz, axis.uy) axis.ry = FixedMul(axis.fz, axis.ux) - FixedMul(axis.fx, axis.uz) axis.rz = FixedMul(axis.fx, axis.uy) - FixedMul(axis.fy, axis.ux) axis.ux = FixedMul(axis.ry, axis.fz) - FixedMul(axis.rz, axis.fy) axis.uy = FixedMul(axis.rz, axis.fx) - FixedMul(axis.rx, axis.fz) axis.uz = FixedMul(axis.rx, axis.fy) - FixedMul(axis.ry, axis.fx) cmo.angle = R_PointToAngle2(0, 0, axis.fx, axis.fy) player.awayviewaiming = -R_PointToAngle2(0, 0, FixedHypot(axis.fx, axis.fy), axis.fz) player.viewrollangle = computeRoll(axis, flipped) if player.realmo then player.realmo.angle = 0 end if player == consoleplayer then P_ResetCamera(consoleplayer, camera) end player.aiming = 0 end -- Standard Camera Rotation (First Person Style) local function rotateCamera(player) local cam = player.speccam local cmd = player.cmd local cmo = cam.mo local yaw = cmo.angle local pitch = player.awayviewaiming local roll = player.viewrollangle local sensitivity = FU local mousex, mousey = cmd.angleturn, cmd.aiming local sinroll, cosroll = sin(roll), cos(roll) -- Rotate input based on roll so "Up" on mouse still feels like "Up" on screen local local_x = FixedMul(cosroll, mousex) - FixedMul(sinroll, mousey) local local_y = FixedMul(sinroll, mousex) + FixedMul(cosroll, mousey) local pitchDeg = AngleFixed(pitch) / FU if pitchDeg >= 90 and pitchDeg <= 270 then local_x = -local_x end if abs(local_x) <= 8 then local_x = 0 end if abs(local_y) <= 8 then local_y = 0 end local_x, local_y = cinemaSmoothing(cam, local_x, local_y) pitch = $ + (local_y * sensitivity) yaw = $ + FixedMul(local_x * sensitivity, abs(cos(pitch))) roll = handleRoll(player, roll) cmo.angle = yaw player.awayviewaiming = pitch player.viewrollangle = roll if player.realmo then player.realmo.angle = 0 end player.aiming = 0 end -- Helper: Calculate Distance and Angles (H/V) to a target object local function DistAngleToObject(source, target, use_height) if not IsValid(1, source) or not IsValid(1, target) then return nil end local z_offset = target.z if use_height then z_offset = target.z + (target.height * P_MobjFlip(target)) end local angle = source.angle local horizontal_angle = R_PointToAngle2(source.x, source.y, target.x, target.y) local distance = R_PointToDist2(source.x, source.y, target.x, target.y) local vertical_angle = R_PointToAngle2(0, 0, distance, z_offset - source.z) return distance, horizontal_angle, vertical_angle end -- Rotates the camera around the tracked object (Orbiting) local function RotateAround(player, h_move) local cmd_p = player.cmd local speccam = player.speccam local pmo, smo, omo = player.realmo, speccam.mo, speccam.object local R_Angle1 = R_PointToAngle2(smo.x, smo.y, omo.x, omo.y) local cosang, sinang = cos(R_Angle1), sin(R_Angle1) if (smo.flags & MF_ENEMY) then smo.flags = $ & ~MF_ENEMY end -- Zoom In/Out logic using WeaponPrev/Next if (cmd_p.buttons & BT_WEAPONPREV) then if smo.sm_dist.xy < 1024*omo.scale then P_TryMove(smo, smo.x-FixedMul(omo.scale, cosang)*32, smo.y-FixedMul(omo.scale, sinang)*32, true) smo.sm_dist.xy = R_PointToDist2(smo.x, smo.y, omo.x, omo.y) end cmd_p.buttons = $ - BT_WEAPONPREV elseif (cmd_p.buttons & BT_WEAPONNEXT) then if smo.sm_dist.xy > 32*omo.scale then P_TryMove(smo, smo.x+FixedMul(omo.scale, cosang)*32, smo.y+FixedMul(omo.scale, sinang)*32, true) smo.sm_dist.xy = R_PointToDist2(smo.x, smo.y, omo.x, omo.y) end cmd_p.buttons = $ - BT_WEAPONNEXT end -- Height adjustment (Orbit Up/Down) if player.aiming ~= 0 then if player.aiming > 0 then if smo.sm_dist.z < 512*omo.scale then P_MoveOrigin(smo, smo.x, smo.y, smo.z + FixedMul(omo.scale, cmd_p.aiming)*512) else smo.sm_dist.z = 512*omo.scale end elseif player.aiming < 0 then if smo.sm_dist.z > -512*omo.scale then P_MoveOrigin(smo, smo.x, smo.y, smo.z + FixedMul(omo.scale, cmd_p.aiming)*512) else smo.sm_dist.z = -512*omo.scale end end end -- Horizontal Orbit if (h_move/FU) > 5 or (h_move/FU) < -5 then local _, horizontal_angle, _ = DistAngleToObject(omo, smo, true) local new_angle = horizontal_angle + FixedMul(omo.scale, h_move) local radius = smo.sm_dist.xy/FU local new_x = omo.x + cos(new_angle) * radius local new_y = omo.y + sin(new_angle) * radius P_TryMove(smo, new_x, new_y, true) end smo.sm_dist.x, smo.sm_dist.y, smo.sm_dist.z = smo.x - omo.x, smo.y - omo.y, smo.z - omo.z local _, horizontal_angle, vertical_angle = DistAngleToObject(smo, omo, true) player.awayviewaiming = vertical_angle pmo.angle = horizontal_angle smo.angle = horizontal_angle player.aiming = 0 smo.flags = $ + MF_ENEMY end local vs_sm = false local nextplayer = nil local change_chase = 5 addHook("ThinkFrame", function() -- Handle automatic camera switching between players if vs_sm == true then COM_BufInsertText(player, "spectator_mode player "..#nextplayer) displayplayer = consoleplayer vs_sm = false end -- Adjust FOV smoothly for console player if IsValid(1, consoleplayer) then if consoleplayer.speccam then local fov = CV_FindVar("fov").value if change_chase > 0 then change_chase = $ - 1 end consoleplayer.fovadd = consoleplayer.speccam.fov+(90*FU-fov) else change_chase = 5 end end -- Sync camera position for non-chase Object Mode for player in players.iterate do if IsValid(2, player, player.realmo) and player.speccam and IsValid(1, player.speccam.mo) then if player.speccam.type == "object" and IsValid(1, player.speccam.object) and not player.speccam.chase then local obj = player.speccam.object local z_pos = obj.z + obj.height - (obj.scale * 29) if (obj.flags2 & MF2_OBJECTFLIP) then z_pos = obj.z - obj.height + (obj.scale * 29) end P_SetOrigin(player.speccam.mo, obj.x, obj.y, z_pos) end end end end) -- Main logic for "Object" tracking mode (Camera follows a specific mobj) local function UpdateObjectCamera(player, speccam, pmo, smo, cmd_p) local omo = speccam.object -- If object is gone, revert to free camera if not IsValid(1, omo) then speccam.type = "freecamera" player.speccam.d_angle = player.drawangle pmo.angle = 0 cmd_p.angleturn = 0 speccam.blockplayer = true smo.sm_dist = nil return end -- First Person / Fixed View if not speccam.chase then smo.angle = omo.angle if IsValid(1, omo.player) then player.awayviewaiming = omo.player.aiming if player == consoleplayer then displayplayer = omo.player end end if player == consoleplayer then omo.dontdrawforviewmobj = smo -- Force camera chase cvar reset if needed if camera.chase == true and change_chase == 0 then change_chase = 5 COM_BufInsertText(consoleplayer, "spectator_mode chase") end end else -- Third Person / Chasing View if player == consoleplayer then omo.dontdrawforviewmobj = nil if camera.chase == false and change_chase == 0 then change_chase = 5 COM_BufInsertText(consoleplayer, "spectator_mode chase") end end if not (player.pflags & PF_ANALOGMODE) player.pflags = $ | PF_ANALOGMODE end if not (player.pflags & PF_DIRECTIONCHAR) player.pflags = $ | PF_DIRECTIONCHAR end -- Toggles via Custom Buttons if (cmd_p.buttons & BT_CUSTOM1) and not speccam.c1_p then speccam.follow = not speccam.follow if not speccam.follow then smo.sm_dist = nil end end if (cmd_p.buttons & BT_CUSTOM2) and not speccam.c2_p then speccam.t_angle = not speccam.t_angle end if (cmd_p.buttons & BT_CUSTOM3) and not speccam.c3_p then speccam.blockplayer = not speccam.blockplayer end if speccam.follow then -- Calculate offsets if they don't exist if smo.sm_dist == nil then smo.sm_dist = { x = smo.x - omo.x, y = smo.y - omo.y, z = smo.z - omo.z, xy = R_PointToDist2(smo.x, smo.y, omo.x, omo.y), xyz = R_PointToDist2(smo.x, smo.y, omo.x, omo.y) + abs(smo.z - omo.z) } end local distance, horizontal_angle, vertical_angle = DistAngleToObject(smo, omo, true) if distance ~= nil then player.awayviewaiming, smo.angle, pmo.angle = vertical_angle, horizontal_angle, horizontal_angle end -- Wall collision logic for camera: Move closer if line of sight is broken if smo.sm_dist.xy < 1024*omo.scale and P_CheckSight(smo, omo) then P_MoveOrigin(smo, smo.x, smo.y, omo.z + smo.sm_dist.z) if (smo.flags & MF_ENEMY) then smo.flags = $ & ~MF_ENEMY end P_TryMove(smo, omo.x + smo.sm_dist.x, omo.y + smo.sm_dist.y, true) smo.flags = $ | MF_ENEMY end if (smo.sm_dist.xy > 1024*omo.scale and P_CheckSight(smo, omo)) or not P_CheckSight(smo, omo) then P_MoveOrigin(smo, smo.x+(cos(horizontal_angle)*128), smo.y+(sin(horizontal_angle)*128), smo.z) if P_CheckSight(smo, omo) then smo.sm_dist.x = $ + (cos(horizontal_angle)*128) smo.sm_dist.y = $ + (sin(horizontal_angle)*128) smo.sm_dist.xy = R_PointToDist2(smo.x, smo.y, omo.x, omo.y) end end if smo.sm_dist.z > 512*omo.scale then smo.sm_dist.z = 512*omo.scale end -- Manual rotation around object if (cmd_p.buttons & BT_FIRENORMAL) and speccam.chase then cmd_p.forwardmove, cmd_p.sidemove = 0, 0 RotateAround(player, (cmd_p.angleturn*FU)-horizontal_angle) end else -- Logic for when camera is NOT strictly following but looking at object local PosZ = omo.z + (P_MobjFlip(omo) * 128 * omo.scale) if not speccam.ps2 then speccam.ps2 = {x = omo.x, y = omo.y, z = PosZ} end local ps2 = speccam.ps2 local distance, horizontal_angle, vertical_angle = DistAngleToObject(smo, omo, true) local h_ang = smo.angle - horizontal_angle local v_ang = player.awayviewaiming - vertical_angle -- Reset if too far or sight lost if not P_CheckSight(smo, omo) or R_PointToDist2(omo.x, omo.y, smo.x, smo.y) > 3072*omo.scale or (cmd_p.buttons & BT_ATTACK) then local dist = R_PointToDist2(omo.x, omo.y, ps2.x, ps2.y) if dist > 256*FRACUNIT or speccam.prevps2 == nil then P_SetOrigin(smo, ps2.x, ps2.y, ps2.z) else P_SetOrigin(smo, speccam.prevps2.x, speccam.prevps2.y, speccam.prevps2.z) end end -- Snap back if angles get too extreme if abs(h_ang/FU) > 10000 or abs(v_ang/FU) > 7000 then P_SetOrigin(smo, ps2.x, ps2.y, ps2.z) smo.angle = horizontal_angle player.awayviewaiming = P_MobjFlip(omo) == -1 and ANG30 or -ANG30 end if (P_MobjFlip(omo) == -1 and omo.z - 128*omo.scale < smo.z) or (P_MobjFlip(omo) ~= -1 and omo.z + 128*omo.scale > smo.z) then P_SetOrigin(smo, smo.x, smo.y, PosZ) end if distance > 512*omo.scale then speccam.prevps2 = speccam.ps2 speccam.ps2 = {x = omo.x, y = omo.y, z = PosZ} end if (cmd_p.forwardmove ~= 0 or cmd_p.sidemove ~= 0) and distance < 128*omo.scale then speccam.ps2.moved = true end end -- Angle Tracking: Sync camera angle with object angle if speccam.t_angle then if speccam.follow then if (smo.flags & MF_ENEMY) then smo.flags = $ & ~MF_ENEMY end local dist = smo.sm_dist.xy/FU P_TryMove(smo, omo.x-(cos(omo.angle)*dist), omo.y-(sin(omo.angle)*dist), true) smo.sm_dist.x, smo.sm_dist.y, smo.sm_dist.z = smo.x - omo.x, smo.y - omo.y, smo.z - omo.z smo.angle = R_PointToAngle2(smo.x, smo.y, omo.x, omo.y) smo.flags = $ | MF_ENEMY else local _, horizontal_angle, vertical_angle = DistAngleToObject(smo, omo, true) smo.angle, player.awayviewaiming = horizontal_angle, vertical_angle end end if not speccam.follow and speccam.ps2 and speccam.ps2.moved and cmd_p.sidemove == 0 and cmd_p.forwardmove == 0 then speccam.ps2.moved = false end if speccam.follow or (not speccam.follow and speccam.ps2 and not speccam.ps2.moved) then player.aiming = 0 pmo.angle = smo.angle end -- Ensure flags are correct for visibility/interaction if (smo.flags & MF_NOCLIP) then smo.flags = $ - MF_NOCLIP end if (smo.flags & MF_NOCLIPHEIGHT) then smo.flags = $ - MF_NOCLIPHEIGHT end if not (smo.flags & MF_NOGRAVITY) then smo.flags = $ + MF_NOGRAVITY end end end -- Main logic for "Free Camera" mode local function UpdateFreeCamera(player, speccam, pmo, smo, cmd_p) if player == consoleplayer then camera.chase = true end if not speccam.axis then speccam.axis = { fx=FU, fy=0, fz=0, rx=0, ry=FU, rz=0, ux=0, uy=0, uz=FU } end -- Handle Rotation if speccam.sixDOF then rotateCamera6DOF(player) else rotateCamera(player) end player.drawangle = speccam.d_angle -- Fire "Catch in 4K" projectile (Object Selector) smo.c_delay = smo.c_delay or 0 if smo.c_delay > 0 then smo.c_delay = $ - 1 end if (cmd_p.buttons & BT_ATTACK) and smo.c_delay == 0 then P_SpawnMissileAngle(smo, 128, smo.angle, player.awayviewaiming, MT_CATCHIN4K) smo.c_delay = 10 end -- Toggles via WeaponMask buttons (Ring Toss) if (cmd_p.buttons & BT_WEAPONMASK) == 1 and not speccam.w_p then speccam.cinema = not speccam.cinema end if (cmd_p.buttons & BT_WEAPONMASK) == 2 and not speccam.w_p then speccam.glide = not speccam.glide end if (cmd_p.buttons & BT_WEAPONMASK) == 3 and not speccam.w_p then speccam.sixDOF = not speccam.sixDOF if speccam.sixDOF then speccam.axis = anglesToAxis(smo, player) end end if (cmd_p.buttons & BT_WEAPONMASK) == 4 and not speccam.w_p then speccam.noclip = not speccam.noclip end if (cmd_p.buttons & BT_WEAPONMASK) == 5 and not speccam.w_p then speccam.gravity = not speccam.gravity end -- Speed Control (Holding C1 + Weapon Prev/Next) if (cmd_p.buttons & BT_CUSTOM1) then if (cmd_p.buttons & BT_WEAPONNEXT) and speccam.speed < 1000 then speccam.speed = $ + 10 elseif (cmd_p.buttons & BT_WEAPONPREV) and speccam.speed > 0 then speccam.speed = $ - 10 end end -- Reset view roll/pitch if (cmd_p.buttons & BT_CUSTOM2) then player.viewrollangle, player.awayviewaiming = 0, 0 if speccam.sixDOF then speccam.axis = anglesToAxis(smo, player) end end -- Move camera if keys pressed if cmd_p.forwardmove ~= 0 or cmd_p.sidemove ~= 0 or (cmd_p.buttons & BT_JUMP) or (cmd_p.buttons & BT_SPIN) then moveCamera(player) speccam.movexy = 0 if not speccam.gravity then speccam.movez = 0 end end -- Glide physics (Momentum retention) if speccam.glide then local maxspeed = (speccam.speed * FU) / 4 local hspeed = R_PointToDist2(0, 0, smo.momx, smo.momy) local vspeed = abs(smo.momz) local abs_speed = hspeed + vspeed if abs_speed > maxspeed then local scale = FixedDiv(maxspeed, abs_speed) smo.momx = FixedMul(smo.momx, scale) smo.momy = FixedMul(smo.momy, scale) smo.momz = FixedMul(smo.momz, scale) else local damp_val = speccam.gravity and (FU*31/32) or (FU*30/32) smo.momx = Damp(smo.momx, damp_val, FU/256) smo.momy = Damp(smo.momy, damp_val, FU/256) if not speccam.gravity then smo.momz = Damp(smo.momz, damp_val, FU/256) end end end -- Noclip handling if speccam.noclip then if not (smo.flags & MF_NOCLIP) then smo.flags = $ + MF_NOCLIP end if not (smo.flags & MF_NOCLIPHEIGHT) then smo.flags = $ + MF_NOCLIPHEIGHT end else if (smo.flags & MF_NOCLIP) then smo.flags = $ - MF_NOCLIP end if (smo.flags & MF_NOCLIPHEIGHT) then smo.flags = $ - MF_NOCLIPHEIGHT end end -- Gravity handling if speccam.gravity then speccam.movez = 2 player.viewrollangle = 0 speccam.sixDOF = false if (smo.flags & MF_NOGRAVITY) then smo.flags = $ - MF_NOGRAVITY end else if not (smo.flags & MF_NOGRAVITY) then smo.flags = $ + MF_NOGRAVITY end end end -- ========================================================================= -- MAIN LOOP: PreThinkFrame -- Handles input detection and state switching before physics runs -- ========================================================================= addHook("PreThinkFrame", function() if not sirexer_spectator_mode then sirexer_spectator_mode = true end for player in players.iterate do if IsValid(2, player, player.realmo) and player.speccam and IsValid(1, player.speccam.mo) then local speccam = player.speccam local cmd_p = player.cmd local pmo = player.realmo local smo = speccam.mo -- Only allow spectator mode in Coop, or if player is actually a spectator if not G_CoopGametype() then if player.spectator then speccam.movexy, speccam.movez = 0, 0 else RemoveCamera(player) return end end -- Drag/Damping timers if speccam.movexy > 0 then speccam.movexy = $ - 1 elseif not speccam.glide then smo.momx, smo.momy = 0, 0 end if speccam.movez > 0 then speccam.movez = $ - 1 elseif not speccam.glide then smo.momz = 0 end -- Override player view to camera object player.awayviewmobj = smo player.awayviewtics = 5 -- FOV Adjustment (Weapon Next/Prev buttons) if (speccam.type == "freecamera" and not (cmd_p.buttons & BT_CUSTOM1)) or (speccam.type == "object" and not (cmd_p.buttons & BT_FIRENORMAL)) then if (cmd_p.buttons & BT_WEAPONPREV) and speccam.fov < 88*FU then speccam.fov = $ + 4*FU elseif (cmd_p.buttons & BT_WEAPONNEXT) and speccam.fov > -88*FU then speccam.fov = $ - 4*FU end end -- Switch Freecamera -> Tracking Self (Custom 3) if (cmd_p.buttons & BT_CUSTOM3) and not speccam.c3_p and speccam.type == "freecamera" and IsValid(1, player.mo) then speccam.type = "object" pmo.angle, player.viewrollangle = player.drawangle, 0 speccam.noclip, speccam.gravity, speccam.cinema, speccam.glide, speccam.sixDOF, speccam.blockplayer = false, false, false, false, false, false speccam.object = pmo local _, horizontal_angle, vertical_angle = DistAngleToObject(smo, pmo, true) player.awayviewaiming, smo.angle, pmo.angle = vertical_angle, horizontal_angle, horizontal_angle if player == consoleplayer then camera.chase = true change_chase = 5 end end -- Switch Object -> Freecamera (WeaponMask 1 / Ring Toss) if (cmd_p.buttons & BT_WEAPONMASK) == 1 and not speccam.w_p and speccam.type == "object" then speccam.type = "freecamera" speccam.d_angle, pmo.angle, cmd_p.angleturn = player.drawangle, 0, 0 speccam.chase, speccam.blockplayer = true, true smo.sm_dist = nil end -- State Machine execution if speccam.type == "object" then UpdateObjectCamera(player, speccam, pmo, smo, cmd_p) elseif speccam.type == "freecamera" then UpdateFreeCamera(player, speccam, pmo, smo, cmd_p) end -- Toggle HUD visibility if (cmd_p.buttons & BT_TOSSFLAG) and not speccam.tf_p then speccam.hud = not speccam.hud end -- Track button presses to prevent rapid toggling speccam.c1_p = (cmd_p.buttons & BT_CUSTOM1) and true or false speccam.c2_p = (cmd_p.buttons & BT_CUSTOM2) and true or false speccam.c3_p = (cmd_p.buttons & BT_CUSTOM3) and true or false speccam.tf_p = (cmd_p.buttons & BT_TOSSFLAG) and true or false speccam.w_p = (cmd_p.buttons & BT_WEAPONMASK) and true or false -- Fix flags if object flip is involved if (smo.flags2 & MF2_OBJECTFLIP) then smo.flags2 = $ - MF2_OBJECTFLIP end if (smo.eflags & MFE_VERTICALFLIP) then smo.eflags = $ - MFE_VERTICALFLIP end -- Block player input if active if speccam.blockplayer then cmd_p.forwardmove, cmd_p.sidemove, cmd_p.buttons = 0, 0, 0 end -- Force noclip off if server disallows it if sm_allownoclip.value == 0 then speccam.noclip = false end elseif player.speccam then RemoveCamera(player) else player.speccam = nil end end end) -- Command: Initialize freecamera, optionally at coordinates local cmd_sm_freecamera = function(player, args) local pmo = player.realmo local x, y, z = pmo.x, pmo.y, pmo.z local dontmove = false local last_arg if player.speccam then x, y, z = player.speccam.mo.x, player.speccam.mo.y, player.speccam.mo.z end for _, v in ipairs(args) do local lower_v = string.lower(v) if string.match(lower_v, "^%-r?[xyz]$") then last_arg = lower_v else local n = tonumber(v) if n ~= nil and last_arg then local val = n * FU dontmove = true if last_arg == "-x" then x = val elseif last_arg == "-y" then y = val elseif last_arg == "-z" then z = val elseif last_arg == "-rx" then x = x + val elseif last_arg == "-ry" then y = y + val elseif last_arg == "-rz" then z = z + val end last_arg = nil end end end if not player.speccam then SpawnCamera(player, pmo, x, y, z) local smo = player.speccam.mo if not dontmove then P_InstaThrust(smo, pmo.angle, -8*FU) end player.aiming = 0 player.speccam.d_angle = player.drawangle if (pmo.flags2 & MF2_OBJECTFLIP) then player.viewrollangle = ANGLE_180 if not dontmove then P_SetObjectMomZ(smo, -2*FU) end else player.viewrollangle = 0 if not dontmove then P_SetObjectMomZ(smo, 2*FU) end end elseif player.speccam and dontmove then P_MoveOrigin(player.speccam.mo, x, y, z) else RemoveCamera(player) end end -- Command: Initialize object tracking logic local cmd_sm_object = function(player, args) if player.speccam then RemoveCamera(player) end local object = player.mo if args[2] ~= nil then local target = findplayer(args[2]) if target then object = target.mo else CONS_Printf(player, "There is no player named \""..args[2].."\""); return end end if not IsValid(1, object) then if args[2] ~= nil then CONS_Printf(player, "The player is probably not entered in the game or does not exist") else CONS_Printf(player, "That's not gonna work :3") end return end local chase = true if args[3] ~= nil then local arg3 = string.lower(args[3]) if arg3 == "off" or arg3 == "false" or arg3 == "no" or arg3 == "0" then chase = false if player == consoleplayer then camera.chase = false end end end SpawnCamera(player, object) local speccam = player.speccam local smo = speccam.mo speccam.type, speccam.blockplayer, speccam.object, smo.target, speccam.chase = "object", false, object, player.realmo, chase if player == consoleplayer then camera.chase = chase end player.aiming, speccam.d_angle = 0, player.drawangle if chase then P_MoveOrigin(smo, smo.x - cos(object.angle)*168, smo.y - sin(object.angle)*168, smo.z) local offset = (object.flags2 & MF2_OBJECTFLIP) and -64*FU or 64*FU if not (object.flags2 & MF2_OBJECTFLIP) then player.viewrollangle = 0 end P_MoveOrigin(smo, smo.x, smo.y, smo.z + offset) else local offset = (object.flags2 & MF2_OBJECTFLIP) and -object.height or object.height P_MoveOrigin(smo, smo.x, smo.y, smo.z + offset) end end -- Console Command: spectator_mode COM_AddCommand("spectator_mode", function(player, ...) local args = {...} if args[1] ~= nil and string.lower(args[1]) == "help" then local ht = "///Spectator Mode\\\\\\\nVersion: 1.1\nAuthor: Sirexer\n" ht = ht .. "The camera can be enabled by pressing the F6 button\nCommands:\n" ht = ht .. "spectator_mode freecamera - enables the camera\n" ht = ht .. "spectator_mode freecamera -x -y -z - enables the camera and teleports the camera directly to the specified coordinates\n" ht = ht .. "spectator_mode freecamera -rx -ry -rz - enables the camera and teleports the camera relative to camera or player current location\n" ht = ht .. "spectator_mode player - enables the camera in tracking object mode" CONS_Printf(player, ht) return end if not G_CoopGametype() and not player.spectator then CONS_Printf(player, "Doesn't work when you're in game and in non friendly gametypes") return end if IsValid(2, player, player.realmo) then if args[1] ~= nil and string.lower(args[1]) == "player" then cmd_sm_object(player, args) elseif args[1] ~= nil and string.lower(args[1]) == "chase" then -- Toggle chase mode for object tracking if IsValid(1, player) and player.speccam then local speccam, smo, omo = player.speccam, player.speccam.mo, player.speccam.object if speccam.chase then speccam.chase = false if player == consoleplayer then camera.chase = false change_chase = 5 end else smo.sm_dist = nil P_MoveOrigin(smo, smo.x - cos(omo.angle)*128, smo.y - sin(omo.angle)*128, smo.z) local offset = (omo.flags2 & MF2_OBJECTFLIP) and -64*FU or 64*FU if not (omo.flags2 & MF2_OBJECTFLIP) then player.viewrollangle = 0 end P_MoveOrigin(smo, smo.x, smo.y, smo.z + offset) speccam.chase = true if player == consoleplayer then camera.chase = true change_chase = 5 end cmd_sm_object(player, "player", #player) end end else cmd_sm_freecamera(player, args) end end end) -- Handle switching views when cycling players in standard spectator mode addHook("ViewpointSwitch", function(player, nextp, forced) if not IsValid(2, player, player.realmo) or not player.speccam then return end if not IsValid(1, nextplayer) and IsValid(2, nextp, nextp.mo) then nextplayer = nextp else local selectnext, selectn1 = false, true for p in players.iterate do if not selectnext and p == nextplayer then selectnext = true elseif selectnext then if IsValid(2, p, p.mo) then nextplayer, selectn1 = p, false break else selectnext = false end end end if selectnext and selectn1 then for p in players.iterate do if IsValid(2, p, p.mo) then nextplayer = p break end end end end if not IsValid(1, nextplayer) or not IsValid(1, nextplayer.mo) then nextplayer = nil end if nextplayer ~= nil then vs_sm = true end end) -- HUD Drawing Function local function spectator_hud(v) local player = consoleplayer if IsValid(2, player, player.realmo) and player.speccam and IsValid(1, player.speccam.mo) then if hud.enabled("textspectator") then hud.disable("textspectator") end local speccam = player.speccam if not speccam.hud then return end local smo = speccam.mo local roll, angle, aim = AngleFixed(player.viewrollangle)/FU, AngleFixed(smo.angle)/FU, AngleFixed(player.awayviewaiming)/FU local px, py, pz = smo.x/FU, smo.y/FU, smo.z/FU local speed_f = speccam.speed/100 local speed = speed_f.."."..(speccam.speed - speed_f*100)/10 local vel = (R_PointToDist2(0,0,smo.momx,smo.momy)+abs(smo.momz))/FU local fov = speccam.fov/FU+88 local function flag(b) return b and "[ON ]" or "[OFF]" end -- ========================= -- RIGHT PANEL (Camera Data) -- ========================= local ry = 0 local function drawR(text, offset) v.drawString(320, ry, text, V_SNAPTORIGHT|V_SNAPTOTOP, "small-right") ry = ry + offset end drawR("== CAMERA ==", 8) drawR("Yaw : "..angle, 4) drawR("Pitch: "..aim, 4) drawR("Roll : "..roll, 8) drawR("== POSITION ==", 8) drawR("X: "..px, 4) drawR("Y: "..py, 4) drawR("Z: "..pz, 8) drawR("FOV : "..fov, 4) if speccam.type == "freecamera" then drawR("Speed: x"..speed, 4) drawR("Vel : "..vel, 4) end -- ========================= -- LEFT PANEL (Mode / Info) -- ========================= local ly = 88 local function drawL(text, offset) v.drawString(0, ly, text, V_SNAPTOLEFT|V_SNAPTOBOTTOM, "small") ly = ly + offset end drawL("Toss Flag - Toggle HUD", 8) drawL("=== MODE / CONTROLS ===", 8) if speccam.type == "freecamera" then drawL("1 - Cinema "..flag(speccam.cinema), 4) drawL("2 - Glide "..flag(speccam.glide), 4) drawL("3 - 6DOF "..flag(speccam.sixDOF), 4) drawL("4 - Noclip "..flag(speccam.noclip), 4) drawL("5 - Gravity "..flag(speccam.gravity), 8) drawL("C1 - Speed Up", 4) drawL("C2 - Center View", 4) drawL("C3 - Tracking Self", 8) drawL("Weapon Next/Prev - FOV Control", 4) drawL("C1+Weapon Next/Prev - Speed Control", 4) drawL("FN+Move Left/Right - Roll Camera", 4) drawL("F12 - Tracking Player", 4) elseif speccam.type == "object" then drawL("Follow "..flag(speccam.follow), 4) drawL("Track Angle "..flag(speccam.t_angle), 4) drawL("Block Player "..flag(speccam.blockplayer), 8) drawL("F12 - Tracking Player", 4) drawL("1 - Switch to Freecamera", 4) if speccam.chase then drawL("C1 - Object Tracking "..flag(speccam.follow), 4) drawL("C2 - Angle Tracking "..flag(speccam.t_angle), 4) drawL("C3 - Block Player Movement "..flag(speccam.blockplayer), 8) drawL("FN+Mouse - Change Perspective", 4) end end end end hud.add(spectator_hud) -- Logic to push the camera away from walls slightly addHook("MobjMoveBlocked", function(mo, thing, line) if not IsValid(2, mo, line) then return end if (mo.flags & MF_ENEMY) then mo.flags = $ & ~MF_ENEMY P_TryMove(mo, mo.x+mo.momx, mo.y+mo.momy, true) mo.flags = $ | MF_ENEMY end end, MT_SPECCAMERA) addHook("KeyDown", function(key) if key.name == "f6" and not key.repeated then COM_BufInsertText(consoleplayer, "spectator_mode") end end) -- Collision Logic: CatchIn4K projectile hitting an object addHook("MobjMoveCollide", function(mo, tmthing) if not IsValid(2, mo, tmthing) or not IsValid(1, mo.source) or not IsValid(1, mo.source.target) or not IsValid(1, mo.source.target.player) then return end local flip = P_MobjFlip(tmthing) local z_top = tmthing.z + (flip == 1 and (tmthing.height + 4*tmthing.scale) or (-4*tmthing.scale)) local z_bottom = tmthing.z + (flip == 1 and (-4*tmthing.scale) or (-tmthing.height - 4*tmthing.scale)) if mo.z > z_top or mo.z < z_bottom then return end -- Check if the hit thing is valid for tracking (Player, Enemy, Boss) if tmthing.player or (tmthing.flags & MF_ENEMY) or (tmthing.flags & MF_BOSS) or (tmthing.target and ((tmthing.target.flags & MF_ENEMY) or (tmthing.target.flags & MF_BOSS))) then if not (tmthing.flags & MF_ENEMY) and not (tmthing.flags & MF_BOSS) and tmthing.target and ((tmthing.target.flags & MF_ENEMY) or (tmthing.target.flags & MF_BOSS)) then tmthing = tmthing.target end local player = mo.source.target.player if player then RemoveCamera(player) end -- Switch the player to tracking this new object SpawnCamera(player) local speccam = player.speccam local smo = speccam.mo speccam.type, speccam.blockplayer, speccam.object, smo.target = "object", false, tmthing, player.realmo P_MoveOrigin(smo, tmthing.x - cos(speccam.object.angle)*168, tmthing.y - sin(speccam.object.angle)*168, tmthing.z) player.aiming, player.speccam.d_angle = 0, player.drawangle if (speccam.object.flags2 & MF2_OBJECTFLIP) then P_MoveOrigin(smo, smo.x, smo.y, tmthing.z - tmthing.height - 64*speccam.object.scale) else player.viewrollangle = 0 P_MoveOrigin(smo, smo.x, smo.y, tmthing.z + tmthing.height + 64*speccam.object.scale) end P_RemoveMobj(mo) if player == consoleplayer then camera.chase = true change_chase = 5 end end end, MT_CATCHIN4K) -- Cleanup the projectile if it stops moving or hits ground addHook("MobjThinker", function(mo) if not IsValid(1, mo) then return end if (abs(mo.momx/FU) < 64 and abs(mo.momy/FU) < 64 and abs(mo.momz/FU) < 64) or P_IsObjectOnGround(mo) or not IsValid(1, mo.source) or not IsValid(1, mo.source.target) or not IsValid(1, mo.source.target.player) or mo.source.target.player.speccam.type == "object" then if mo.fuse == 0 then mo.fuse = 1 end end end, MT_CATCHIN4K) addHook("MapLoad", function() for player in players.iterate do player.speccam = nil end end) addHook("MobjDeath", function(mo) return true end, MT_SPECCAMERA)