-- todo -- add support for bots local EXTRA_LARGE_BUBBLE_RESPAWN_TIME = 2 * TICRATE local EXTRA_LARGE_BUBBLE_INITIAL_SPAWN_TIME = 4 * TICRATE local EXTRA_LARGE_BUBBLE_SPAWN_MOMZ = 2 * FRACUNIT + FRACUNIT / 2 local EXTRA_LARGE_BUBBLE_MOMZ_DECAY = FixedDiv(-1 * FRACUNIT, 20 * FRACUNIT) local EXTRA_LARGE_BUBBLE_HOVER_LIMIT = FRACUNIT / 2 local EXTRA_LARGE_BUBBLE_HOVER_DIVISOR = 50 * FRACUNIT local EXTRA_LARGE_BUBBLE_DEATH_STATE = mobjinfo[MT_EXTRALARGEBUBBLE].deathstate local BUBBLE_SOUNDS = { sfx_bubbl1; sfx_bubbl2; sfx_bubbl3; sfx_bubbl4; sfx_bubbl5; } states[S_BUBBLES1] = { sprite = SPR_BBLS; frame = 0; tics = 8; action = nil; var1 = 0; var2 = 0; nextstate = S_BUBBLES2; } states[S_BUBBLES2] = { sprite = SPR_BBLS; frame = 1; tics = 8; action = nil; var1 = 0; var2 = 0; nextstate = S_BUBBLES3; } states[S_BUBBLES3] = { sprite = SPR_BBLS; frame = 2; tics = 8; action = nil; var1 = 0; var2 = 0; nextstate = S_BUBBLES4; } states[S_BUBBLES4] = { sprite = SPR_BBLS; frame = 3; tics = 8; action = nil; var1 = 0; var2 = 0; nextstate = S_BUBBLES1; } -- port of P_GetFFloorTopZAt from p_slopes.c -- Returns the height of the FOF top at (x, y) local function getFFloorTopZAt(ffloor, x, y) if ffloor.t_slope ~= nil then return P_GetSlopeZAt(ffloor.t_slope, x, y) else return ffloor.topheight end end -- port of P_GetFFloorBottomZAt from p_slopes.c -- Returns the height of the FOF bottom at (x, y) local function getFFloorBottomZAt(ffloor, x, y) if ffloor.b_slope ~= nil then return P_GetSlopeZAt(ffloor.b_slope, x, y) else return ffloor.bottomheight end end -- port of P_SceneryCheckWater from p_mobj.c local function sceneryCheckWater(mobj) -- see if we are in water, and set some flags for later local sector = mobj.subsector.sector -- Default if no water exists. mobj.watertop = mobj.z - 1000 * FRACUNIT mobj.waterbottom = mobj.watertop if sector.ffloors ~= nil then mobj.eflags = $ & ~(MFE_UNDERWATER|MFE_TOUCHWATER) for rover in sector.ffloors() do if not (rover.flags & FF_EXISTS) or not (rover.flags & FF_SWIMMABLE) or rover.flags & FF_BLOCKOTHERS then continue end local topheight = getFFloorTopZAt(rover, mobj.x, mobj.y) local bottomheight = getFFloorBottomZAt(rover, mobj.x, mobj.y) if topheight <= mobj.z or bottomheight > (mobj.z + (mobj.height >> 1)) then continue end if mobj.z + mobj.height > topheight then mobj.eflags = $ | MFE_TOUCHWATER else mobj.eflags = $ & ~MFE_TOUCHWATER end -- Set the watertop and waterbottom mobj.watertop = topheight mobj.waterbottom = bottomheight if mobj.z + (mobj.height >> 1) < topheight then mobj.eflags = $ | MFE_UNDERWATER else mobj.eflags = $ & ~MFE_UNDERWATER end end else mobj.eflags = $ & ~(MFE_UNDERWATER | MFE_TOUCHWATER) end end addHook("MobjSpawn", function(bubblePatch) bubblePatch.cooldown = EXTRA_LARGE_BUBBLE_INITIAL_SPAWN_TIME + P_RandomRange(0, 35) end, MT_BUBBLES) addHook("MobjThinker", function(bubblePatch) if bubblePatch.eflags & MFE_UNDERWATER then bubblePatch.flags2 = $ & ~MF2_DONTDRAW else bubblePatch.flags2 = $ | MF2_DONTDRAW return false end if leveltime % 8 == 0 then local bubble local randomNumber = P_RandomRange(1, 8) if randomNumber >= 6 then bubble = P_SpawnMobj( bubblePatch.x, bubblePatch.y, bubblePatch.z + bubblePatch.height / 2, MT_SMALLBUBBLE ) elseif randomNumber == 5 then bubble = P_SpawnMobj( bubblePatch.x, bubblePatch.y, bubblePatch.z + bubblePatch.height / 2, MT_MEDIUMBUBBLE ) end if bubble then bubble.scale = bubblePatch.scale end end local extraLargeBubble = bubblePatch.extralargebubble if extraLargeBubble ~= nil and not extraLargeBubble.valid then extraLargeBubble = nil end if extraLargeBubble == nil then if bubblePatch.cooldown ~= nil and bubblePatch.cooldown > 0 then bubblePatch.cooldown = $ - 1 else extraLargeBubble = P_SpawnMobj( bubblePatch.x, bubblePatch.y, bubblePatch.z + bubblePatch.height / 2, MT_EXTRALARGEBUBBLE ) bubblePatch.extralargebubble = extraLargeBubble extraLargeBubble.bubblepatch = bubblePatch extraLargeBubble.scale = bubblePatch.scale extraLargeBubble.state = S_LARGEBUBBLE1 P_SetObjectMomZ(extraLargeBubble, EXTRA_LARGE_BUBBLE_SPAWN_MOMZ, false) S_StartSound(bubblePatch, BUBBLE_SOUNDS[P_RandomRange(1, #BUBBLE_SOUNDS)]) end end end, MT_BUBBLES) addHook("MobjThinker", function(extraLargeBubble) if extraLargeBubble.state == EXTRA_LARGE_BUBBLE_DEATH_STATE then return false end if extraLargeBubble.bubblepatch ~= nil then if extraLargeBubble.bubblepatch.valid then sceneryCheckWater(extraLargeBubble) if not (extraLargeBubble.eflags & MFE_UNDERWATER) then P_RemoveMobj(extraLargeBubble) return false end if extraLargeBubble.state == S_LARGEBUBBLE1 or extraLargeBubble.state == S_LARGEBUBBLE2 then extraLargeBubble.tics = $ - 1 if extraLargeBubble.tics == 0 then P_SetMobjStateNF(extraLargeBubble, states[extraLargeBubble.state].nextstate) end elseif extraLargeBubble.state == S_EXTRALARGEBUBBLE then if extraLargeBubble.hoverdirection ~= nil then P_SetObjectMomZ( extraLargeBubble, FixedDiv(extraLargeBubble.hoverdirection, EXTRA_LARGE_BUBBLE_HOVER_DIVISOR), true ) if abs(extraLargeBubble.momz) >= FixedMul(EXTRA_LARGE_BUBBLE_HOVER_LIMIT, extraLargeBubble.scale) then extraLargeBubble.hoverdirection = $ * -1 end else P_SetObjectMomZ(extraLargeBubble, EXTRA_LARGE_BUBBLE_MOMZ_DECAY, true) if extraLargeBubble.momz <= 0 then P_SetObjectMomZ(extraLargeBubble, 0, false) extraLargeBubble.hoverdirection = FRACUNIT * -1 end end if extraLargeBubble.momx ~= 0 or extraLargeBubble.momy ~= 0 then P_XYMovement(extraLargeBubble) end if extraLargeBubble.momz ~= 0 then P_ZMovement(extraLargeBubble) end end else extraLargeBubble.bubblepatch = nil end return true end end, MT_EXTRALARGEBUBBLE) -- this is to prevent bots from becoming stuck in air bubbles; in vanilla, bots -- do not delete air bubbles when touching them, but they still refill their air -- as a compromise, i'll make it so that bots will delete bubbles, but the next -- bubble will spawn immediately afterwards addHook("MobjCollide", function(extraLargeBubble, playerMobj) local bubblePatch = extraLargeBubble.bubblepatch if bubblePatch ~= nil and bubblePatch.valid then local player = playerMobj.player if player ~= nil and player.bot ~= 0 and playerMobj.state == S_PLAY_GASP then extraLargeBubble.immediaterespawn = true P_KillMobj(extraLargeBubble) end end end, MT_EXTRALARGEBUBBLE) addHook("MobjRemoved", function(extraLargeBubble) local bubblePatch = extraLargeBubble.bubblepatch if bubblePatch ~= nil and bubblePatch.valid then if extraLargeBubble.immediaterespawn then bubblePatch.cooldown = 0 else bubblePatch.cooldown = EXTRA_LARGE_BUBBLE_RESPAWN_TIME end end end, MT_EXTRALARGEBUBBLE)