if (RaidBosses != nil) // Already loaded. return; end // DEFINE GLOBAL TABLE rawset(_G, "RaidBosses", {}); RaidBosses.CountPlaying = function() if not (gametyperules & GTR_FRIENDLY) // Act like unmodified. return 1; end local numPlaying = 0; for player in players.iterate() do if (player.bot) // Don't count bots. continue; end if (player.spectator and not player.outofcoop) // Don't count intentional spectators, // but DO count Game Overs continue; end numPlaying = $1 + 1; end return numPlaying; end RaidBosses.FixedLog2 = function(x) local precision = FRACBITS - 1; // need to reduce to fit into INT32 local b = 1 << (precision - 1); local y = 0; if (x == 0) return INT32_MIN; end while (x < 1 << precision) x = $1 << 1; y = $1 - (1 << precision); end while (x >= 2 << precision) x = $1 >> 1; y = $1 + (1 << precision); end local z = x; for i = 0,precision-1 do z = z * z >> precision; if (z >= 2 << precision) z = $1 >> 1; y = $1 + b; end b = $1 >> 1; end return (y << 1) - FRACUNIT; // and then fudge here to get back to actual result end RaidBosses.IsRaidBoss = function(boss) if not (gametyperules & GTR_FRIENDLY) return false; end if not (boss and boss.valid) return false; end if (boss.flags & MF_BOSS) and (boss.info.spawnhealth > 1) return true; end return false; end local MAXPLAYERS = #players; RaidBosses.HPMultiplier = function(playingInt, miniBoss) if (playingInt < 2) return FRACUNIT; end local playing = playingInt * FRACUNIT; local mul = (playing + FRACUNIT) - RaidBosses.FixedLog2(playing); if (miniBoss) // mini bosses have an inbetween multiplier local delta = mul - FRACUNIT; mul = $1 - (delta / 2); end return mul; end RaidBosses.PlayerCountUpdate = function(boss, newCount) /* for i = 2,MAXPLAYERS do print(string.format("%d-player: %f", i, RaidBosses.HPMultiplier(i))); end */ boss.raidBoss.playing = newCount; boss.raidBoss.damage = FRACUNIT; if (boss.raidBoss.playing > 1) local mul = RaidBosses.HPMultiplier(boss.raidBoss.playing, (boss.info.raid_miniboss == true)); if (mul > FRACUNIT) boss.raidBoss.damage = FixedDiv(FRACUNIT, mul); end end end RaidBosses.Init = function(boss) if (boss.raidBoss == nil) boss.raidBoss = {}; end boss.raidBoss.poise = FRACUNIT; RaidBosses.PlayerCountUpdate(boss, RaidBosses.CountPlaying()); boss.raidBoss.perPlayerFret = {}; for i = 0,MAXPLAYERS do boss.raidBoss.perPlayerFret[i] = 0; end boss.raidBoss.prevFlash = false; boss.raidBoss.actualColor = boss.color; boss.raidBoss.actualColorized = boss.colorized; end RaidBosses.Damage = function(boss, inflictor, source, dmg, dmgType) if (RaidBosses.IsRaidBoss(boss) == false) return false; end if (boss.flags2 & MF2_FRET) return true; end if (boss.raidBoss == nil) RaidBosses.Init(boss); end local pID = 0; // 0 is used for environment damage if (source and source.valid) and (source.player and source.player.valid) pID = #(source.player) + 1; end if (boss.raidBoss.perPlayerFret[pID] > 0) return true; else S_StartSound(boss, sfx_kc2a); boss.raidBoss.perPlayerFret[pID] = 24; end boss.raidBoss.poise = $1 - (boss.raidBoss.damage * dmg); if (boss.raidBoss.poise <= 0) // Deal actual damage, reset poise. boss.raidBoss.poise = FRACUNIT; return false; end return true; end RaidBosses.IsDisplayPlayer = function(player) if not (player and player.valid) return false; end if (displayplayer and displayplayer.valid and player == displayplayer) return true; elseif (secondarydisplayplayer and secondarydisplayplayer.valid and player == secondarydisplayplayer) return true; end return false; end RaidBosses.Think = function(boss) if (RaidBosses.IsRaidBoss(boss) == true) if (boss.raidBoss == nil) and (boss.target and boss.target.valid and boss.target.health > 0) RaidBosses.Init(boss); end if (boss.raidBoss != nil) local flash = false; for i = 0,MAXPLAYERS do if (boss.flags2 & MF2_FRET) boss.raidBoss.perPlayerFret[i] = 1; elseif (boss.raidBoss.perPlayerFret[i] > 0) boss.raidBoss.perPlayerFret[i] = $1 - 1; if (i > 0 and RaidBosses.IsDisplayPlayer(players[i - 1])) flash = true; end end end if (flash == true and (leveltime & 1)) // Why can't we access actual flashing colors...? boss.color = SKINCOLOR_SILVER; boss.colorized = true; elseif (boss.raidBoss.prevFlash == true) // Restore previously tracked color boss.color = boss.raidBoss.actualColor; boss.colorized = boss.raidBoss.actualColorized; else // Track proper color boss.raidBoss.actualColor = boss.color; boss.raidBoss.actualColorized = boss.colorized; end boss.raidBoss.prevFlash = flash; local updateFreq = TICRATE; // Logarithm can be slow, so let's not update every tic. if (leveltime % updateFreq == 0) local count = RaidBosses.CountPlaying(); if (count != boss.raidBoss.playing) RaidBosses.PlayerCountUpdate(boss, count); end end end end return false; end local function DoAbilityBounce(player, changeMomZ) if (player.mo.state == S_PLAY_BOUNCE_LANDING) return; end if (changeMomZ) local prevMomZ = player.mo.momz; local minMomZ = 0; if (P_MobjFlip(player.mo) * prevMomZ < 0) prevMomZ = 0; elseif (player.mo.eflags & MFE_UNDERWATER) prevMomZ = $1 / 2; end P_DoJump(player, false); player.pflags = $1 & ~(PF_STARTJUMP|PF_JUMPED); minMomZ = FixedMul(player.mo.momz, 3*FRACUNIT/2); if (player.mo.eflags & MFE_VERTICALFLIP) // Use "min" or "max" depending on if the player is flipped player.mo.momz = min(minMomZ, (minMomZ + prevMomZ)/2); else player.mo.momz = max(minMomZ, (minMomZ + prevMomZ)/2); end end S_StartSound(player.mo, sfx_boingf); player.mo.state = S_PLAY_BOUNCE_LANDING; player.pflags = $1 | PF_BOUNCING|PF_THOKKED; end local function TwinSpinRejuvenate(player, tType) local actionspd = 0; local movang = 0; local ang = 0; local fa = 0; if not (player.mo and player.mo.valid) or (tType == MT_NULL) return; end actionspd = FixedMul(player.actionspd, player.mo.scale); fa = R_PointToAngle2(0, 0, player.mo.momz, FixedHypot(player.mo.momx, player.mo.momy)); movang = R_PointToAngle2(0, 0, player.mo.momx, player.mo.momy); ang = 0; v = FixedMul(actionspd, sin(fa)); h = actionspd - FixedMul(actionspd, cos(fa)); for i = 1,8 do local side = actionspd - FixedMul(h, abs(sin(ang))); local xo = P_ReturnThrustX(nil, ang + movang, side); local yo = P_ReturnThrustY(nil, ang + movang, side); local zo = -FixedMul(cos(ang), v); local missle = P_SpawnMobjFromMobj( player.mo, xo, yo, (player.mo.height / 2) + zo, tType ); missile.target = player.mo; missile.destscale = $1 >> 1; missile.scale = missile.destscale; missile.angle = ang + movang; missile.fuse = TICRATE/2; missile.extravalue2 = (99*FRACUNIT)/100; missile.momx = xo; missile.momy = yo; missile.momz = zo; ang = $1 + ANGLE_45; end player.pflags = $1 & ~PF_THOKKED; end RaidBosses.Touch = function(boss, other) local player = other.player; local elementalPierce = 0; if (player.pflags & PF_SHIELDABILITY) if ((player.powers[pw_shield] & SH_NOSTACK) == SH_ELEMENTAL) elementalPierce = 1; elseif ((player.powers[pw_shield] & SH_NOSTACK) == SH_BUBBLEWRAP) elementalPierce = 2; end end if (P_PlayerCanDamage(player, boss) == true) if (P_MobjFlip(other) * other.momz < 0) and (elementalPierce != 1) if not (player.charability2 == CA2_MELEE and player.panim == PA_ABILITY2) local setmomz = -other.momz; // Store this, momz get changed by P_DoJump within P_DoBubbleBounce if (elementalPierce == 2) // Reset bubblewrap, part 1 P_DoBubbleBounce(player); end other.momz = setmomz; if (elementalPierce == 2) // Reset bubblewrap, part 2 local underwater = (other.eflags & MFE_UNDERWATER); // Cap the height! if (underwater) other.momz = $1 / 2; other.momz = $1 - ($1 / 8); else other.momz = $1 - ($1 / 4); end end end end if (player.pflags & PF_BOUNCING) DoAbilityBounce(player, false); end if (boss.info.spawnhealth > 1) // Multi-hit? Bounce back! other.momx = -other.momx; other.momy = -other.momy; if (player.charability == CA_FLY and player.panim == PA_ABILITY) other.momz = -other.momz/2; elseif (player.pflags & PF_GLIDING and not P_IsObjectOnGround(other)) player.pflags = $1 & ~(PF_GLIDING|PF_JUMPED|PF_NOJUMPDAMAGE); other.state = S_PLAY_FALL; other.momz = $1 + P_MobjFlip(other) * (player.speed >> 3); other.momx = 7 * $1 >> 3; other.momy = 7 * $1 >> 3; elseif (player.dashmode >= DASHMODE_THRESHOLD and (player.charflags & (SF_DASHMODE|SF_MACHINE)) == (SF_DASHMODE|SF_MACHINE) and player.panim == PA_DASH) P_DoPlayerPain(player, boss, boss); end end P_DamageMobj(boss, other, other, 1, 0); if (player.charability == CA_TWINSPIN and player.panim == PA_ABILITY) TwinSpinRejuvenate(player, player.thokitem); end else P_DamageMobj(other, boss, boss, 1, 0); end end RaidBosses.Collide = function(other, boss) if (RaidBosses.IsRaidBoss(boss) == true) and (other.player and other.player.valid) if (boss.health <= 0) or (other.health <= 0) return false; end if (other.player.spectator) return false; end if (other.momz < 0) if (other.z + other.momz > boss.z + boss.height) return false; end elseif (other.z > boss.z + boss.height) return false; end if (other.momz > 0) if (other.z + other.height + other.momz < boss.z) return false; end elseif (other.z + other.height < boss.z) return false; end if (boss.raidBoss != nil) local pID = #(other.player) + 1; if (boss.raidBoss.perPlayerFret[pID] > 0) return false; end end RaidBosses.Touch(boss, other); return false; end return nil; end addHook("MobjDamage", RaidBosses.Damage); addHook("BossThinker", RaidBosses.Think); addHook("MobjCollide", RaidBosses.Collide, MT_PLAYER); addHook("MobjMoveCollide", RaidBosses.Collide, MT_PLAYER);