-- Tatsuru's Post-exit Movement (ExitMove) Script. -- You're being watched. -- Version 1.6.3 -- Compatibility with the newest Utility -- Warp command revamped rawset(_G, "exitmove", {}) -- Wow! freeslot("MT_PLRGOAL", "MT_CAMERA", "S_PLAYERFIN1", "S_PLAYERFIN2", "S_PLAYERFIN3", "S_PLAYERFIN4") -- Let there be light! mobjinfo[MT_PLRGOAL] = { doomednum = -1, spawnstate = S_PLAYERFIN1, radius = 10*FRACUNIT, height = 8*FRACUNIT, speed = 4*FRACUNIT, flags = MF_NOGRAVITY|MF_FLOAT|MF_NOCLIPHEIGHT } mobjinfo[MT_CAMERA] = { doomednum = -1, spawnstate = S_INVISIBLE, radius = 5*FRACUNIT, height = 5*FRACUNIT, flags = MF_NOGRAVITY|MF_SLIDEME } states[S_PLAYERFIN1] = { sprite = SPR_GOAL, frame = A|TR_TRANS40, tics = 35, nextstate = S_PLAYERFIN2 } states[S_PLAYERFIN2] = { sprite = SPR_GOAL, frame = B|TR_TRANS40, tics = 35, nextstate = S_PLAYERFIN3 } states[S_PLAYERFIN3] = { sprite = SPR_GOAL, frame = C|TR_TRANS40, tics = 35, nextstate = S_PLAYERFIN4 } states[S_PLAYERFIN4] = { sprite = SPR_GOAL, frame = D|TR_TRANS40, tics = 35, nextstate = S_PLAYERFIN1 } local finishcount local endlevel local starposts = {} local exitplayers local freeze local mapnum local warp = true local gameovertimer = 10*TICRATE local ThisIsABossLevel = false local NoMoreExits = false local dummy local camera -- Is the sector you're in an exit sector or an "exit" sector? local function findExit(player, sector) if not player.finished -- Only the first to finish actually does it. if sector.special / 4096 == 2 player.finished = true elseif sector.special >= -12288 and sector.special < -8192 -- The others are only fake finish. player.finished = true player.exiting = 0 print(player.name+" has completed the level.") -- Let's search for custom exits. local line = P_FindSpecialLineFromTag(2, sector.tag) if line >= 0 if lines[line].flags & ML_BLOCKMONSTERS if All7Emeralds(emeralds) G_SetCustomExitVars(lines[line].frontsector.ceilingheight/FRACUNIT) else G_SetCustomExitVars(lines[line].frontsector.floorheight/FRACUNIT) end else G_SetCustomExitVars(lines[line].frontsector.floorheight/FRACUNIT) end end end end end local function canIWarp(player) if not warp return false, "Warping is currently disabled by the server." elseif endlevel return false elseif utility and player.utility and player.utility.afk return false, "You cannot warp while AFK." else return true end end addHook("ThinkFrame", function() if gametype == GT_COOP and not (maptol & TOL_NIGHTS) -- Failsafe for bosses and Egg Capsules. If everyone living has the same exiting value at the same tic, it means a boss got defeated. local able = false for player in players.iterate if not endlevel -- Just to make sure this does NOT run on levels with exit sectors if player.playerstate == PST_LIVE and player.exiting == 0 and not player.finished able = true end end end if not able and not endlevel ThisIsABossLevel = true else ThisIsABossLevel = false end -- Get rid of the exit sectors so the dumbasses can move for player in players.iterate if not endlevel if player.finished if player.mo.subsector.sector.special / 4096 == 2 player.mo.subsector.sector.special = $ + (4096*11) end for fof in player.mo.subsector.sector.ffloors() if player.mo.z <= fof.topheight and player.mo.z + player.mo.height >= fof.bottomheight if player.mo.subsector.sector.special / 4096 == 2 player.mo.subsector.sector.special = $ + (4096*11) end end end -- Leave no exit sector unturned if not NoMoreExits for sector in sectors.iterate if sector.special / 4096 == 2 sector.special = $ + (4096*11) end end NoMoreExits = true end end -- DON'T GET STUCK ANYWHERE EVER THERE'S NOT EVEN ANY EXIT SECTORS ANYMORE OH MY GOD if not ThisIsABossLevel if utility and player.utility if not player.utility.afk player.exiting = 0 end else player.exiting = 0 end end end -- Is the sector you're in an exit sector? findExit(player, player.mo.subsector.sector) for fof in player.mo.subsector.sector.ffloors() if player.mo.z <= fof.topheight and player.mo.z + player.mo.height >= fof.bottomheight findExit(player, fof.sector) end end -- If the finished player is alive and in the server, give them the light. if player.finished and player.playerstate == PST_LIVE and not player.light player.light = {} for i = 1, 3 local light = P_SpawnMobj(player.mo.x, player.mo.y, player.mo.z, MT_PLRGOAL) light.target = player.mo light.scale = FRACUNIT/2 light.angle = FixedAngle(((360/3)*i)*FRACUNIT) table.insert(player.light, light) end end -- For stopping the timer properly. if not (player.finished or player.lives <= 0) player.finishtime = player.realtime end -- Game-overed players can't finish the level. if player.lives <= 0 and player.finished player.finished = false end -- Finished players cannot drown, and time stops for them. if player.finished player.realtime = player.finishtime player.powers[pw_underwater] = 20*TICRATE player.powers[pw_spacetime] = 20*TICRATE end -- So people stop spamming warp. if not player.starpostdelay player.starpostdelay = 0 elseif player.starpostdelay > 0 player.starpostdelay = $ - 1 end -- If a smart guy happens to join when everyone's finished if endlevel and player.exiting == 0 player.exiting = 99 end end local playercount = 0 local nonplayer = 0 local afkplayer = 0 -- Counter for Utility AFKs finishcount = 0 -- Compare if the number of players that have finished matches the number of active and alive players in the server. if not endlevel for player in players.iterate playercount = $ + 1 if player.finished finishcount = $ + 1 end if player.lives <= 0 nonplayer = $ + 1 elseif utility and player.utility if player.utility.afk and not player.finished nonplayer = $ + 1 afkplayer = $ + 1 end end end end -- Cut the number of players according to the command determined by the server. if exitplayers == "One" --Cut the number of players according to the command determined by the server. playercount = 1 elseif exitplayers == "Half" if nonplayer > playercount / 2 playercount = $ - nonplayer else playercount = $ / 2 end elseif exitplayers == "3/4" if nonplayer > playercount / 4 * 3 playercount = $ - nonplayer else playercount = $ / 4 * 3 end else exitplayers = "All" playercount = $ - nonplayer end -- If the number of finished players corresponds to the number of players who can finish the level, end it. if not endlevel and leveltime > 0 and finishcount > 0 and playercount > 0 and finishcount >= playercount and not freeze if exitplayers == "All" and nonplayer == 0 print ("All players have completed the level.") else print ("Enough players have completed the level.") end endlevel = true for player in players.iterate player.exiting = 99 end elseif playercount == 0 and nonplayer > 1 and gameovertimer > 0 and afkplayer == 0 -- All our players are game-overed gameovertimer = $ - 1 else gameovertimer = 10*TICRATE end -- Game over. Restart the map. if gameovertimer == 0 and not freeze G_ExitLevel(mapnum, true) gameovertimer = 10*TICRATE end -- For viewing starposts before warping for player in players.iterate if player.warpview player.powers[pw_nocontrol] = TICRATE player.awayviewtics = TICRATE local canwarp, reason = canIWarp(player) if not canwarp if reason then CONS_Printf(player, reason) end player.powers[pw_nocontrol] = 0 player.awayviewtics = 0 player.warpview = false continue end if player.cmd.buttons & BT_FIRENORMAL S_StartSound(nil, sfx_gravch, player) P_TeleportMove(player.mo, starposts[player.starpost].x, starposts[player.starpost].y, starposts[player.starpost].z) player.starpostdelay = TICRATE player.awayviewtics = 0 player.powers[pw_nocontrol] = 0 player.warpview = false end if player.cmd.buttons & BT_TOSSFLAG player.starpostdelay = TICRATE player.awayviewtics = 0 player.powers[pw_nocontrol] = 0 player.warpview = false end end end if dummy and dummy.valid and dummy.light if not camera for _, light in ipairs(dummy.light) if light.valid and not (light.flags2 & MF2_DONTDRAW) light.flags2 = $ | MF2_DONTDRAW end end else for _, light in ipairs(dummy.light) if light.valid and light.flags2 & MF2_DONTDRAW light.flags2 = $ & ~MF2_DONTDRAW end end end end end end) addHook("MapLoad", function(map) if gametype == GT_COOP and not (maptol & TOL_NIGHTS) COM_BufInsertText(server, "playersforexit all") mapnum = map ThisIsABossLevel = false NoMoreExits = false -- Because maps don't actually reset for sector in sectors.iterate if sector.special >= -12288 and sector.special < -8192 sector.special = $ - (4096*11) end end -- Store all Star Posts. starposts = {} for thing in mapthings.iterate if thing.type == 502 table.insert(starposts, thing.mobj) end end -- Reset the detectors at every map load. endlevel = false for player in players.iterate player.exiting = 0 player.warpview = false player.starpostdelay = TICRATE player.powers[pw_underwater] = 20*TICRATE player.starpost = #starposts player.finished = false player.light = nil player.finishtime = 0 end end end) -- Warping magic! local function toStarpost(player) if player.starpostdelay > 0 return end if #starposts == 0 CONS_Printf(player, "There are no Star Posts in this level.") return end if player.starpost and player.starpost > 1 player.starpost = $1 - 1 else player.starpost = #starposts end local starpost = starposts[player.starpost] S_StartSound(nil, sfx_menu1, player) player.awayviewmobj = P_SpawnMobj(starpost.x + FixedMul(48*FRACUNIT, cos(starpost.angle)), starpost.y + FixedMul(48*FRACUNIT, sin(starpost.angle)), starpost.z + starpost.height/2, MT_CAMERA) player.awayviewmobj.angle = R_PointToAngle2(player.awayviewmobj.x, player.awayviewmobj.y, starpost.x, starpost.y) player.warpview = true player.starpostdelay = 5 end -- The goal icon now spins around the player. addHook("MobjThinker", function(mo) if mo.target and mo.target.player.finished A_RotateSpikeBall(mo) mo.z = $ - mo.target.height/3 else P_RemoveMobj(mo) end end, MT_PLRGOAL) -- No more watching Starposts addHook("MobjThinker", function(mo) local beingViewed = false for player in players.iterate if mo == player.awayviewmobj beingViewed = true end end if not beingViewed P_RemoveMobj(mo) return end end, MT_CAMERA) -- Finished players cannot lose rings when hurt. addHook("ShouldDamage", function(targetmo, _, _, damage) if gametype == GT_COOP and not (maptol & TOL_NIGHTS) if damage == 10000 and targetmo.player.exiting == 0 if not utility or (utility and targetmo.player.utility and not targetmo.player.utility.afk) -- Suicide return true end end if targetmo.player != nil and targetmo.player.finished and targetmo.player.powers[pw_flashing] == 0 and not endlevel if not utility or (utility and targetmo.player.utility and not targetmo.player.utility.afk) S_StartSound(targetmo, sfx_shldls) P_RemoveShield(targetmo.player) P_DoPlayerPain(targetmo.player) return false end end end end, MT_PLAYER) -- Finished players that die won't lose lives. addHook("MobjDeath", function(mo) if gametype == GT_COOP and not (maptol & TOL_NIGHTS) if mo.player.finished mo.player.lives = $1 + 1 mo.player.light = nil end end end, MT_PLAYER) COM_AddCommand("FREEZE", function(player) if not freeze freeze = true CONS_Printf(player, "Freeze has been activated.") CONS_Printf(player, "The current level will not end until it's deactivated again.") else CONS_Printf(player, "Freeze has been deactivated.") freeze = false end end, 1) COM_AddCommand("REMAINING", function(player) if server == nil CONS_Printf(player, "You must be in a game to use this command.") end if gametype != GT_COOP or (maptol & TOL_NIGHTS) CONS_Printf(player, "This command cannot be used in this gametype.") end CONS_Printf(player, "Players who haven't finished the level:") for plr in players.iterate if not plr.finished and plr.lives > 0 -- Don't count game-overed players as players. if not utility or (utility and plr.utility and not plr.utility.afk) CONS_Printf(player, " "..plr.name) end end end end, 0) --This allows the server to control which fraction of the players is necessary to finish the level. COM_AddCommand("CUSTOMEXIT", function(player, arg) if server == nil CONS_Printf(player, "You must be in a game to use this command.") return false end if not arg CONS_Printf(player, "Sets how many players are needed to finish the level.") CONS_Printf(player, "Available options are One, Half, 3/4 and All.") CONS_Printf(player, "Currently set to "..exitplayers..", default is All.") elseif string.lower(arg) == "one" or string.lower(arg) == "1" exitplayers = "One" elseif string.lower(arg) == "half" or string.lower(arg) == "1/2" exitplayers = "Half" elseif string.lower(arg) == "3/4" exitplayers = "3/4" elseif string.lower(arg) == "all" exitplayers = "All" else CONS_Printf(player, "Invalid option. Currently set to "..exitplayers..".") end end, 1) COM_AddCommand("WARP", function(player, arg) if arg if server and player != server and not IsPlayerAdmin(player) or (utility and not utility.IsAdmin(player)) CONS_Printf(player, "Only the server can toggle this feature.") elseif string.lower(arg) == "on" if not warp warp = true CONS_Printf(player, "Warping has been enabled.") end elseif string.lower(arg) == "off" if warp warp = false CONS_Printf(player, "Warping has been disabled.") end else if warp CONS_Printf(player, "Invalid option. Warping is currently enabled.") else CONS_Printf(player, "Invalid option. Warping is currently disabled.") end end else if server == nil CONS_Printf(player, "You must be in a game to use this command.") elseif not warp CONS_Printf(player, "Warping is currently disabled by the server.") elseif endlevel --CONS_Printf(player, "This level has already ended.") return elseif player.lives <= 0 or player.playerstate == PST_DEAD CONS_Printf(player, "You cannot use this command while dead.") elseif not player.finished CONS_Printf(player, "You haven't finished the level yet.") elseif gametype != GT_COOP or (maptol & TOL_NIGHTS) CONS_Printf(player, "This command cannot be used in this gametype.") elseif utility and player.utility and player.utility.afk CONS_Printf(player, "You cannot warp while AFK.") elseif not P_IsObjectOnGround(player.mo) or player.speed > 0 CONS_Printf(player, "You cannot warp while moving.") else toStarpost(player) end end end, 0) addHook("NetVars", function(net) finishcount = net($) endlevel = net($) exitplayers = net($) starposts = net($) freeze = net($) mapnum = net($) gameovertimer = net($) ThisIsABossLevel = net($) NoMoreExits = net($) warp = net($) end) hud.add(function(v, player, cam) if (not dummy or not dummy.valid) and (leveltime == 0 or player.jointime < leveltime) dummy = player end camera = cam.chase if not camera and player.finished local patch = v.cachePatch(sprnames[SPR_GOAL] + "A0") v.draw(298, 203, patch, V_SNAPTOBOTTOM|V_SNAPTORIGHT) end if player.warpview v.drawString(160, 140, "\x83NORMAL TOSS\x80: Warp here", V_ALLOWLOWERCASE|V_30TRANS, "center") v.drawString(160, 148, "\x81WARP command\x80: View next starpost", V_ALLOWLOWERCASE|V_30TRANS, "center") v.drawString(160, 156, "\x85TOSS FLAG\x80: Exit", V_ALLOWLOWERCASE|V_30TRANS, "center") end end, "game")