Boss fight component for the HexPlatforms map, implementing the Oberon boss encounter and its lifecycle. It handles host-driven state (start, phases, changes, spawning/despawning of boss and adds, health caps, rewards) and the client mirror (health bar, FX, portal visuals) and exposes console commands and network hooks.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace NZombies;
/// <summary>
/// BASALT — THE BOSS FIGHT (2026-09-27). The blue altar sends the players to the boss arena's upper floor
/// (`HexPlatforms.BlueAltar.cs`), and there the beast comes out of the lava: Oberon (`OberonBoss`), in four phases, each
/// harder than the one before. Asked for as *"after the players teleport there, all the zombies despawn, and the round
/// freezes, stays where it is"*, then phase by phase:
///
/// | phase | health | Oberon | with him | the arena |
/// |---|---|---|---|---|
/// | 1 | 100→75% | his swipe and his leap | zombies | as it is |
/// | 2 | 75→50% | and a pulse round him: the Shrieker's daze | and pests | its floor shrunk: the edge sunk in the lava |
/// | 3 | 50→25% | and the bomb barrage | and Shriekers | a ring of hex platforms risen a little out of the floor |
/// | 4 | 25→0% | and the black hole | and napalms and hellhounds | the lava risen over the floor: the platforms alone safe |
///
/// ⛔ THE ARRIVAL: every zombie in the map despawns and the round freezes where it is (<see cref="FreezesRound"/>:
/// `RoundManager` and `AmbientSpecials` stand still) — no spawning, no clear, no next round; from then on the fight spawns
/// its own. A moment of quiet, then he comes up at his spawn and his health bar appears (`BossBarHud`).
///
/// ⛔ WHERE THEY COME FROM: the spawns the user placed in the arena with flag 200 — a flag no door opens, so the rounds never
/// use them — *"the boss spawn is for oberon, the normal spawns are for all other zombie types"* (<see cref="FightSpawnFlag"/>).
/// Each keeps its facing. A spawn the shrunk floor no longer holds is left out; with none placed, he comes up at the floor's
/// edge farthest from the players and they near its edge, as before.
///
/// ⛔ EACH PHASE CHANGE IS A SET PIECE: at the line (75, 50, 25%) he can no longer be hurt; the zombies round the players burn
/// up; he roars and dives into the lava; the arena changes, with a quake and a banner; a Max Ammo drops; some ten seconds to
/// breathe, and he comes back out of the lava with his new moves. No hit carries him past a line (<see cref="BossCap"/>):
/// a phase ends exactly there.
///
/// ⛔ THE KILL, AND THE END — *"After the boss is defeated a floating green orb spawns in the boss spawn point — once a player
/// picks it up all players are teleported back to the map and the easter egg is completed"*: every zombie burns up, the
/// flood goes back into its bed, and a green orb floats over his spawn. E on it: the passage home, the teleporter's arcs on
/// every screen, and everyone in the round put down round the blue altar; the rewards (<see cref="GrantEggRewards"/>); and
/// the Easter egg is done — the round loose again, the arena as it was, the altar sending no more (<see cref="EggComplete"/>).
///
/// ⛔ HIS HEALTH, AND HOW TO HURT HIM — the user's rule (2026-09-27): *"The boss takes 1/10 damage normally — But when it gets
/// hit by the wonder weapon, the 10s debuff deals no damage to it, however while it has that debuff it takes full damage"*;
/// and, as a test, *"250* normal zombie health for the round x number of players in game"*.
/// - His pool is <see cref="BossHealthMultiple"/> walkers' health for the frozen round, times the players in the game
/// (<see cref="BossPool"/>).
/// - Every hit is a tenth (<see cref="BossArmour"/>) — unless the Prisma's fuse, its `resonance` status, burns on him: then it
/// is whole (<see cref="BossCap"/>). The fuse itself deals him nothing (`PrismaChain.Infect`).
/// - A zombie dying beside him with the fuse still burning hands him the rest of it, as it would any zombie: the chain is a
/// way in too.
///
/// ⛔ THE REWARDS, THE EASTER EGG DONE — *"50000 points, 5000 salvage and the best part, pack a punch up to mk 6 and rarity up
/// to Godly with the color red — these are just normal scaling pack a punch and rarity, nothing new — but the best part of
/// all, all players become able to have all the augments of all perks, no longer having the limit of 1 major and 2 minor
/// augments"*:
/// - the GIFTS, to everyone in the game as the orb takes them home: 50,000 points and 5,000 salvage, each machine paying its
/// own player (<see cref="GrantEggRewards"/>). A joiner, who was not there, gets none;
/// - the RULES, for everyone for the rest of the game, a joiner too, at any round — the egg their only gate, *"no other way to
/// do it"*; each reads <see cref="EggComplete"/>: Pack-a-Punch to
/// MK6, the ladders carried one step on (`NZPlayer.PapMaxLevel`); rarity to Godly, red, one more x1.5 (`Rarity.TopTier`);
/// and no limit on a perk's augments — the limit lifted, not the augments given, by the user's word (`PerkAugments.Unlimited`).
/// A new game takes the rules away with the Easter egg, and an MK6 or a Godly kept from it counts as MK5 and Legendary.
///
/// ⛔ DYING IN THE FIGHT, THE USER'S RULE (2026-09-27) — *"when a player dies during the fight they have to wait until the fight
/// ends, once they win the fight and are teleported back everyone is revived — however if all players die they lose the
/// game, its game over"*: the dead stay out, as the frozen round keeps them; the passage home revives everyone
/// (<see cref="RunReturn"/>); and the last one standing bleeding out ends the game, as ever — where he despawns at once and the
/// whole Easter egg goes back to its start (`HexPlatforms.OnGameReset`). The knife he never draws: *"dont use it"*.
///
/// ⛔ WHO OWNS WHAT (INSTRUCTIONS.md, "BUILD FOR MULTIPLAYER"):
/// - the FIGHT — its stage, its phase, since when — is HOST state, MIRRORED (`NZNet.BossFightState`) on every change and to a
/// joiner, with his health as a fraction (`NZNet.BossFightHealth`);
/// - HE and the zombies with him are the host's, spawned there and reaching everyone as zombies do (`ZombieCommands.SpawnAt`);
/// - the ARENA'S CHANGES are LOCAL, each machine drawing them from the mirror (`HexPlatforms.ArenaHazards.cs`); so is the
/// flood's burn, each machine burning its own player;
/// - his PULSE and his PULL are felt on each machine by its own player (`NZNet.OberonPulse`, `NZNet.OberonPull`).
/// </summary>
public sealed partial class HexPlatforms
{
// ══ the stages ═══════════════════════════════════════════════════════════════════════════
/// <summary>
/// The fight's stages: none; the intro — the quiet, then his coming; a phase under way; a phase change — the dive, the
/// arena changing, the breather, his return; won — the green orb floating; the passage home, the orb taken; and done — the
/// Easter egg complete, for the rest of the game.
/// </summary>
const int FightNone = 0, FightIntro = 1, FightPhase = 2, FightChange = 3, FightWon = 4, FightReturn = 5, FightDone = 6;
/// <summary>
/// Is basalt's Easter egg complete — the orb taken, everyone home? EVERY machine, from the mirror, for the rest of the game:
/// what the rewards that are rules rather than gifts ask (<see cref="GrantEggRewards"/>).
/// </summary>
public static bool EggComplete => Instance.IsValid() && Instance._fightShown == FightDone;
/// <summary>HOST — the stage, the phase (1-4; in a change, the one it leads to), and since when.</summary>
int _fight;
int _fightPhase;
TimeSince _fightSince;
/// <summary>MIRROR — the same, as this machine was told (`NZNet.BossFightState`); his health, a fraction; and whether he has come yet.</summary>
int _fightShown, _fightPhaseShown;
TimeSince _fightShownSince;
float _bossHealthShown = 1f;
bool _bossSeenShown;
/// <summary>HOST — the fight as it stands, for `NZNet.PushState` to replay to a joiner.</summary>
public static (int Stage, int Phase, float Since, float Health) BossFightNow => Instance.IsValid()
? (Instance._fight, Instance._fightPhase, (float)Instance._fightSince, Instance._bossHealthSent < 0f ? 1f : Instance._bossHealthSent)
: (0, 0, 0f, 1f);
/// <summary>
/// HOST — does the boss fight freeze the round? `RoundManager` asks before it runs anything, `AmbientSpecials` before it
/// spawns: *"the round freezes, stays where it is"*. From the arrival to a new game, the fight won or not.
/// </summary>
public static bool FreezesRound => Instance.IsValid() && Instance._fight is not (FightNone or FightDone);
/// <summary>
/// Is the boss fight on, as this machine was told — from his arrival to everyone home? EVERY machine, from the mirror: the
/// round bar stands down for it (`RoundBarState`), the round frozen and the fight's zombies not the round's.
///
/// ⚠️ LONGER THAN <see cref="BossBarShown"/>, which is down again from his death: the passage home still has the fight's
/// burning adds about, and a round bar back for those seconds would count them.
/// </summary>
public static bool FightShownOn => Instance.IsValid() && OnBasalt && Instance._fightShown is not (FightNone or FightDone);
/// <summary>
/// How fast what comes for the players in the fight is at the least, as a speed rating (`ZombieAI.OnStart`), or null out of
/// it: each phase faster — a run, a fast run, a sprint, the top sprint. EVERY machine, from the mirror, so a puppet runs as
/// its original does.
/// </summary>
public static float? FightSpeedFloor
{
get
{
var m = Instance;
if ( !m.IsValid() || m._fightShown is not (FightIntro or FightPhase or FightChange) ) return null;
return m._fightPhaseShown switch
{
1 => WalkerAnimations.RunRating,
2 => 56f,
3 => WalkerAnimations.SprintRating,
_ => WalkerAnimations.SuperSprintRating,
};
}
}
/// <summary>Where each phase ends, as a fraction of his health: 75, 50, 25% — and the last, at nothing.</summary>
static float PhaseLine( int phase ) => phase switch { 1 => 0.75f, 2 => 0.5f, 3 => 0.25f, _ => 0f };
// ══ the timeline ═════════════════════════════════════════════════════════════════════════
//
// ⚠️ ONE CLOCK FOR THE HOST AND FOR EVERY SCREEN: seconds since the stage began, mirrored with it. The host drives him and
// the zombies off it; each machine draws the arena's changes off the same numbers (`HexPlatforms.ArenaHazards.cs`).
/// <summary>The quiet before he comes, in seconds.</summary>
const float IntroQuiet = 5f;
/// <summary>When in a phase change the Max Ammo drops, in seconds.</summary>
const float ChangeAmmoAt = 3f;
/// <summary>Phase 2's change: the edge's warning from 1s, its sinking from 5s, over 3s.</summary>
const float ShrinkWarnAt = 1f, ShrinkSinkAt = 5f, ShrinkSinkSeconds = 3f;
/// <summary>Phase 3's change: the platforms rising from 3s, over 3s.</summary>
const float HexRiseAt = 3f, HexRiseSeconds = 3f;
/// <summary>Phase 4's change: the warning at 3s, the flood rising from 8s, over 10s; and after the kill it goes back, over 5s.</summary>
const float FloodWarnAt = 3f, FloodRiseAt = 8f, FloodRiseSeconds = 10f, FloodRecedeSeconds = 5f;
/// <summary>When the arena's change into this phase is done, in seconds into the change.</summary>
static float ChangeDone( int next ) => next switch
{
2 => ShrinkSinkAt + ShrinkSinkSeconds,
3 => HexRiseAt + HexRiseSeconds,
4 => FloodRiseAt + FloodRiseSeconds,
_ => 0f,
};
/// <summary>When he comes back, in seconds into the change: ten seconds' breath, and never before the arena has changed.</summary>
static float ReturnAt( int next ) => MathF.Max( 10f, ChangeDone( next ) + 1f );
// ══ the boss ═════════════════════════════════════════════════════════════════════════════
/// <summary>HOST — Oberon as he stands now: spawned for this phase, or coming back for the next.</summary>
ZombieAI _boss;
/// <summary>
/// HOST — his pool, carried from one of his comings to the next: its size, and what is left of it; and, before his first
/// coming, how much of it he comes with — all of it, but `nz_hex_boss phase N` starts him at N's.
/// </summary>
float _bossMax, _bossLeft;
float _bossFirstFraction = 1f;
/// <summary>HOST — since he was last spawned: a spawn that fails is tried again two seconds on, not every frame.</summary>
TimeSince _bossSpawnedSince;
/// <summary>HOST — his brain found and held; let loose on the phase; and, in a change, the dive begun and his return spawned.</summary>
bool _bossReady, _bossReleased, _bossDiving, _bossBack;
/// <summary>HOST — this change's Max Ammo dropped, and its navmesh built again.</summary>
bool _changeAmmo, _changeNav;
/// <summary>HOST — his health as last sent, and since when.</summary>
float _bossHealthSent = -1f;
TimeSince _bossHealthSince;
static float? _bossHealthMultiple, _bossArmour;
/// <summary>
/// How many walkers' health he has for each player in the game: 250 — the user's test number. `nz_hex_boss hp [x]` sets it,
/// on the host and until a restart, from his next first coming; set the default here once the fight has been played.
/// </summary>
public static float BossHealthMultiple
{
get => Math.Clamp( _bossHealthMultiple ?? 250f, 1f, 100000f );
set => _bossHealthMultiple = value;
}
/// <summary>What he takes of a hit while the Prisma's fuse is not on him: a tenth. `nz_hex_boss armour [x]` sets it.</summary>
public static float BossArmour { get => Math.Clamp( _bossArmour ?? 0.1f, 0f, 1f ); set => _bossArmour = value; }
/// <summary>The share of a hit he takes: all of it while the Prisma's fuse burns on him, <see cref="BossArmour"/> otherwise.</summary>
static float BossDamageShare( bool resonating ) => resonating ? 1f : BossArmour;
/// <summary>Is the Prisma's fuse — its `resonance` status — burning on him?</summary>
static bool BossResonating( GameObject boss ) => boss.IsValid() && StatusEffects.Remaining( boss, "resonance" ) > 0f;
/// <summary>The players in the game: every player's body in the scene, one a connection, in the round or not.</summary>
int PlayersInGame() => Math.Max( 1, PlayerSpawner.AllBodies( Scene ).Count );
/// <summary>His pool: <see cref="BossHealthMultiple"/> walkers' health for the round, times the players in the game.</summary>
float BossPool()
{
var round = Math.Max( 1, RoundManager.Instance?.Round ?? 1 );
// ⚠️ AND THE MATCH'S BOSS HEALTH (the lobby's Difficulty, 2026-10-05); this pool replaces his spawn health, so once
return BossHealthMultiple * ZombieStats.HealthForRound( round ) * PlayersInGame() * Difficulty.BossHealth;
}
/// <summary>The one the send takes? — in the round: the players the fight is scaled for.</summary>
int PlayersInFight() => Math.Max( 1, PlayerSpawner.AllBodies( Scene )
.Count( b => b.Components.Get<NZPlayer>( FindMode.EverythingInSelf ) is NZPlayer p && !p.IsOutOfRound && !p.OutOfRoundNet ) );
/// <summary>
/// Oberon, spawned here — his own entrance, out of the floor — facing as his spawn does, or the players' middle; his pool
/// not yet his: that is set as he is let loose (<see cref="ReleaseBoss"/>), once his own start has run. HOST.
/// </summary>
ZombieAI SpawnBoss( (Vector3 At, Rotation? Facing) where )
{
var at = where.At;
if ( _bossSpawnedSince < 2f ) return null;
_bossSpawnedSince = 0;
var variant = SpecialEnemies.VariantFor( SpecialEnemies.Oberon );
if ( variant is null ) { Log.Warning( "[nz-hex] ⚔ no Oberon to spawn — his variant did not load" ); return null; }
// ⚠️ NO MULTIPLIER: his pool is set as he is let loose (`ReleaseBoss`, `BossPool`), whatever his spawn gave him
var z = ZombieCommands.SpawnAt( Scene, at, variant );
if ( z is null ) return null;
// ⚠️ THE SPAWN'S OWN FACING, as `RoundManager.SpawnBossAt` sets it — the rig's correction is composed in his start
var look = (ArenaLandingMiddle - at).WithZ( 0f );
if ( where.Facing is Rotation facing ) z.WorldRotation = facing;
else if ( look.Length > 1f ) z.WorldRotation = Rotation.LookAt( look.Normal, Vector3.Up );
z.GameObject.Name = "Oberon — basalt's beast";
_bossReady = false;
_bossReleased = false;
NZNet.BossFightFx( FxEmerge, at );
NZNet.HexQuake( 2.5f );
Log.Info( $"[nz-hex] ⚔ OBERON COMES OUT OF THE LAVA at {at:0} — phase {_fightPhase}" );
return z;
}
/// <summary>His brain, once his own start has attached it.</summary>
OberonBoss BossBrain() => _boss.IsValid() ? _boss.Components.Get<OberonBoss>( FindMode.EverythingInSelfAndDescendants ) : null;
/// <summary>His health.</summary>
Health BossHealth() => _boss.IsValid() ? _boss.Components.Get<Health>( FindMode.EverythingInSelfAndAncestors ) : null;
/// <summary>
/// His brain found, and held: no special of his own until he is let loose, no knife — the user's question — and nothing
/// to hurt him through his coming. HOST — every frame until done.
/// </summary>
void ReadyBoss()
{
if ( _bossReady ) return;
var brain = BossBrain();
var hp = BossHealth();
if ( !brain.IsValid() || !hp.IsValid() ) return;
brain.Held = true;
brain.PhaseTwoAt = 0f;
hp.Invulnerable = true;
_bossReady = true;
}
/// <summary>Is he spawned, held, and done coming out — to be let loose?</summary>
bool BossHasCome => _boss.IsValid() && _bossReady && _boss.State is not (ZombieState.Spawning or ZombieState.Dead);
/// <summary>
/// Let him loose on this phase: his pool — the first time <see cref="BossPool"/>, from then on what he carried — his moves
/// for it, and to be hurt again. HOST.
/// </summary>
void ReleaseBoss( int phase )
{
var brain = BossBrain();
var hp = BossHealth();
if ( !brain.IsValid() || !hp.IsValid() ) return;
if ( _bossMax <= 0f )
{
_bossMax = BossPool();
_bossLeft = _bossMax * Math.Clamp( _bossFirstFraction, 0.01f, 1f );
}
hp.Reset( _bossMax );
hp.SetCurrent( _bossLeft );
SetMoves( brain, phase );
brain.Held = false;
hp.Invulnerable = false;
_bossReleased = true;
_bossHealthSent = -1f;
Log.Info( $"[nz-hex] ⚔ PHASE {phase} — Oberon at {_bossLeft:0}/{_bossMax:0}, {PhaseText( phase )}" );
}
/// <summary>
/// His moves in each phase: the leap from the first; the pulse from the second; the barrage from the third; the black hole
/// in the last — and less rest between them each time.
/// </summary>
static void SetMoves( OberonBoss brain, int phase )
{
brain.LeapWeight = 4f;
brain.PulseWeight = phase >= 2 ? 2f : 0f;
brain.BombWeight = phase >= 3 ? 1.5f : 0f;
brain.HoleWeight = phase >= 4 ? 1.5f : 0f;
brain.SkillCooldown = phase switch { 1 => 3f, 2 => 2.5f, 3 => 2f, _ => 1.5f };
}
/// <summary>A phase's moves and company, in words.</summary>
static string PhaseText( int phase ) => phase switch
{
1 => "his swipe and his leap; zombies",
2 => "his swipe, his leap and his pulse; zombies and pests",
3 => "his swipe, his leap, his pulse and his barrage; zombies, pests and Shriekers",
_ => "all his moves and the black hole; zombies, pests, Shriekers, napalms and hellhounds",
};
// ══ where ════════════════════════════════════════════════════════════════════════════════
/// <summary>The floor as it stands, from the host's clock: the upper floor, or, once phase 2's edge has sunk, the shrunk one.</summary>
IReadOnlyList<Vector2> FloorOutlineNow() => SinkT( _fight, _fightPhase, _fightSince ) > 0f ? ArenaShrunkOutline : ArenaStageOutline;
/// <summary>The floor's top as it stands: the shrunk floor stands half a unit over the old one (`HexPlatforms.ArenaHazards.cs`).</summary>
float FloorTopNow() => ArenaFloorTop + ArenaStageHeight + (SinkT( _fight, _fightPhase, _fightSince ) > 0f ? ShrunkLift : 0f);
/// <summary>The in-round players' middle, flat; the arena's middle if there is none.</summary>
Vector2 PlayersMiddle()
{
var at = PlayerSpawner.AllBodies( Scene )
.Select( b => b.Components.Get<NZPlayer>( FindMode.EverythingInSelf ) )
.Where( p => p.IsValid() && !p.IsOutOfRound && InBossArena( p.WorldPosition ) )
.Select( p => new Vector2( p.WorldPosition.x, p.WorldPosition.y ) )
.ToList();
if ( at.Count == 0 ) return ArenaMiddle;
return new Vector2( at.Average( p => p.x ), at.Average( p => p.y ) );
}
/// <summary>
/// The flag the user put on the fight's spawns: 200, which no door opens — so the rounds never spawn there (`DoorLinks`).
/// *"i added them with flag 200 — the boss spawn is for oberon, the normal spawns are for all other zombie types"*.
/// </summary>
const string FightSpawnFlag = "200";
/// <summary>The spawns of a list that carry the fight's flag, standing on the floor as it is now.</summary>
List<SpawnPoint> FightSpawns( List<SpawnPoint> all )
{
if ( all is null || all.Count == 0 ) return new List<SpawnPoint>();
var outline = FloorOutlineNow();
return all.Where( s => s is not null && s.Link == FightSpawnFlag
&& InOutline( outline, new Vector2( s.Position.x, s.Position.y ) ) ).ToList();
}
/// <summary>
/// Where he comes up: at the fight's boss spawn — the one farthest from the players, should there be more — facing as it
/// does. With none, the floor's edge farthest from the players, 200u in from it, facing them; the arena's middle, should
/// that not be on the floor.
/// </summary>
(Vector3 At, Rotation? Facing) EmergencePoint()
{
var them = PlayersMiddle();
var spawn = FightSpawns( ActiveConfig.Current?.BossSpawns )
.OrderByDescending( p => (new Vector2( p.Position.x, p.Position.y ) - them).LengthSquared )
.FirstOrDefault();
if ( spawn is not null ) return (spawn.Position, spawn.Rotation);
var outline = FloorOutlineNow();
var far = outline.OrderByDescending( p => (p - them).LengthSquared ).First();
var inward = ArenaMiddle - far;
var at = inward.Length > 1f ? far + inward * (200f / inward.Length) : ArenaMiddle;
if ( !InOutline( outline, at ) ) at = ArenaMiddle;
return (new Vector3( at.x, at.y, FloorTopNow() + 2f ), null);
}
/// <summary>Where he dives: the floor's edge nearest him and 110u on out over the gap, under the lava — into the raised floor, out of sight.</summary>
Vector3 DivePoint( Vector3 from )
{
var flat = new Vector2( from.x, from.y );
var near = FloorOutlineNow().OrderBy( p => (p - flat).LengthSquared ).First();
var outward = near - ArenaMiddle;
var at = outward.Length > 1f ? near + outward * (110f / outward.Length) : near;
return new Vector3( at.x, at.y, ArenaFloorTop - 60f );
}
/// <summary>
/// Where one of the fight's zombies comes up: at one of the fight's zombie spawns, any of them, facing as it does. With none,
/// near the floor's edge, 70u in from it — climbing out of the lava — and off the hex platforms once they stand, facing the
/// middle; the arena's middle if nowhere is found.
/// </summary>
(Vector3 At, Rotation? Facing) AddSpawnPoint()
{
var spawns = FightSpawns( ActiveConfig.Current?.ZombieSpawns );
if ( spawns.Count > 0 )
{
var spawn = spawns[Game.Random.Next( spawns.Count )];
return (spawn.Position, spawn.Rotation);
}
var outline = FloorOutlineNow();
var hexes = HexT( _fight, _fightPhase, _fightSince ) > 0f;
for ( var tries = 0; tries < 10; tries++ )
{
var i = Game.Random.Next( outline.Count );
var a = outline[i];
var b = outline[(i + 1) % outline.Count];
var p = a + (b - a) * Game.Random.Float( 0f, 1f );
var inward = ArenaMiddle - p;
if ( inward.Length < 1f ) continue;
p += inward * (70f / inward.Length);
if ( !InOutline( outline, p ) ) continue;
if ( hexes && Enumerable.Range( 0, FightHexes ).Any( k => (FightHexCentre( k ) - p).Length < Apothem * 1.25f ) ) continue;
return (new Vector3( p.x, p.y, FloorTopNow() + 2f ), null);
}
return (new Vector3( ArenaMiddle.x, ArenaMiddle.y, FloorTopNow() + 2f ), null);
}
// ══ the zombies with him ═════════════════════════════════════════════════════════════════
/// <summary>The kinds that come with him.</summary>
const int AddWalker = 0, AddPest = 1, AddShrieker = 2, AddNapalm = 3, AddHound = 4, AddKinds = 5;
/// <summary>
/// How many of a kind at once in a phase, and one every how many seconds; none alive, not in that phase: zombies from the
/// first, pests from the second, Shriekers from the third, napalms and hellhounds in the last — and more, and more often,
/// each time. Walkers two more for each player past the first.
/// </summary>
static (int Alive, float Every) AddRule( int phase, int kind ) => (kind, phase) switch
{
(AddWalker, 1) => (8, 2f),
(AddWalker, 2) => (10, 1.6f),
(AddWalker, 3) => (12, 1.3f),
(AddWalker, _) => (12, 1f),
(AddPest, 2) => (6, 3f),
(AddPest, 3) => (6, 2.5f),
(AddPest, >= 4) => (8, 2f),
(AddShrieker, 3) => (1, 25f),
(AddShrieker, >= 4) => (2, 20f),
(AddNapalm, >= 4) => (2, 20f),
(AddHound, >= 4) => (4, 5f),
_ => (0, 0f),
};
/// <summary>HOST — the fight's own zombies, each with its kind; when each kind last came; and those burning up, to go. ⚠️ MADE ON FIRST USE, for the hotload.</summary>
List<(ZombieAI Z, int Kind)> _fightAdds;
TimeSince[] _fightAddSince;
List<(ZombieAI Z, TimeSince Since)> _fightBurning;
/// <summary>The fight's zombies coming, each kind by its rule. HOST — `WatchBossFight`, while a phase is under way.</summary>
void SpawnAdds()
{
_fightAdds ??= new();
_fightAddSince ??= new TimeSince[AddKinds];
_fightAdds.RemoveAll( a => !a.Z.IsValid() || a.Z.State == ZombieState.Dead );
var players = PlayersInFight();
for ( var kind = 0; kind < AddKinds; kind++ )
{
var (alive, every) = AddRule( _fightPhase, kind );
if ( alive <= 0 ) continue;
if ( kind == AddWalker ) alive += 2 * (players - 1);
if ( _fightAddSince[kind] < every ) continue;
if ( _fightAdds.Count( a => a.Kind == kind ) >= alive ) continue;
_fightAddSince[kind] = 0;
var z = SpawnAdd( kind );
if ( z is not null ) _fightAdds.Add( (z, kind) );
}
}
/// <summary>One of the fight's zombies, of this kind, at one of the fight's spawns (<see cref="AddSpawnPoint"/>). HOST.</summary>
ZombieAI SpawnAdd( int kind )
{
var name = kind switch
{
AddPest => SpecialEnemies.Pest,
AddShrieker => SpecialEnemies.Shrieker,
AddNapalm => SpecialEnemies.Napalm,
AddHound => SpecialEnemies.Hellhound,
_ => null,
};
var variant = name is null ? null : SpecialEnemies.VariantFor( name );
if ( name is not null && variant is null ) return null;
var (at, facing) = AddSpawnPoint();
var z = ZombieCommands.SpawnAt( Scene, at, variant );
if ( z is null ) return null;
var look = new Vector3( ArenaMiddle.x - at.x, ArenaMiddle.y - at.y, 0f );
if ( facing is Rotation spawnFacing ) z.WorldRotation = spawnFacing;
else if ( look.Length > 1f ) z.WorldRotation = Rotation.LookAt( look.Normal, Vector3.Up );
return z;
}
/// <summary>
/// Every zombie in the arena but him set alight, and gone a second later — despawned, not killed: no points and no drops
/// at a phase change, or the change would rain powerups. HOST.
/// </summary>
void BurnAdds()
{
_fightBurning ??= new();
foreach ( var z in ZombieAI.All.ToList() )
{
if ( !z.IsValid() || z == _boss || z.State == ZombieState.Dead ) continue;
if ( _fightBurning.Any( b => b.Z == z ) ) continue;
StatusEffects.Apply( z.GameObject, "burn", null, 0f, 2f, tickDamage: 0f );
_fightBurning.Add( (z, (TimeSince)0f) );
}
_fightAdds?.Clear();
}
/// <summary>The burning gone, a second after they caught. HOST — every frame.</summary>
void WatchBurning()
{
if ( _fightBurning is null || _fightBurning.Count == 0 ) return;
for ( var i = _fightBurning.Count - 1; i >= 0; i-- )
{
var (z, since) = _fightBurning[i];
if ( z.IsValid() && since < 1f ) continue;
if ( z.IsValid() && z.GameObject.IsValid() ) z.GameObject.Destroy();
_fightBurning.RemoveAt( i );
}
}
/// <summary>Every zombie in the map gone — despawned, as a new game does (`RoundManager.ClearZombies`): no points, no drops. HOST.</summary>
static void DespawnEveryZombie()
{
foreach ( var z in ZombieAI.All.ToList() )
if ( z.IsValid() && z.GameObject.IsValid() ) z.GameObject.Destroy();
}
// ══ the host's run ═══════════════════════════════════════════════════════════════════════
/// <summary>
/// The fight begins: every zombie in the map despawned, the round frozen, the quiet — his coming is <see cref="IntroQuiet"/>
/// on. HOST — the send's arrival, the first one of a game (`WatchArenaSend`), or `nz_hex_boss start`.
/// </summary>
void StartBossFight()
{
DespawnEveryZombie();
_boss = null;
_bossMax = _bossLeft = 0f;
_bossFirstFraction = 1f;
_bossSpawnedSince = 10f;
_bossReady = _bossReleased = _bossDiving = _bossBack = false;
_fightAdds?.Clear();
_fightBurning?.Clear();
SetFight( FightIntro, 1 );
Log.Info( "[nz-hex] ⚔ THE BOSS FIGHT BEGINS — every zombie despawned and the round frozen; the beast comes out of the"
+ $" lava in {IntroQuiet:0}s" );
}
/// <summary>A new stage, told to everyone. HOST.</summary>
void SetFight( int stage, int phase )
{
_fight = stage;
_fightPhase = phase;
_fightSince = 0;
SendBossFight();
}
void SendBossFight() => NZNet.BossFightState( _fight, _fightPhase, _fightSince );
/// <summary>His health to everyone, when it has moved — ten times a second at most. HOST.</summary>
void SendBossHealth( float fraction )
{
if ( MathF.Abs( fraction - _bossHealthSent ) < 0.001f || _bossHealthSince < 0.1f ) return;
_bossHealthSent = fraction;
_bossHealthSince = 0;
NZNet.BossFightHealth( fraction );
}
/// <summary>The fight run, every frame. HOST — `OnUpdate`.</summary>
void WatchBossFight()
{
WatchBurning();
if ( _fight == FightNone ) return;
if ( !OnBasalt ) { EndBossFight( "not on basalt" ); return; }
if ( _boss.IsValid() ) ReadyBoss();
switch ( _fight )
{
case FightIntro:
if ( !_boss.IsValid() && _fightSince >= IntroQuiet ) _boss = SpawnBoss( EmergencePoint() );
if ( BossHasCome )
{
ReleaseBoss( 1 );
SetFight( FightPhase, 1 );
}
break;
case FightPhase:
RunPhase();
break;
case FightChange:
RunChange();
break;
case FightReturn:
RunReturn();
break;
}
}
/// <summary>
/// A phase under way: its zombies coming; he — spawned again should anything have taken him — let loose once he has come;
/// his health watched: at the phase's line, the change; at nothing, the kill. HOST.
/// </summary>
void RunPhase()
{
SpawnAdds();
if ( !_boss.IsValid() ) { _boss = SpawnBoss( EmergencePoint() ); return; }
if ( !_bossReleased )
{
if ( BossHasCome ) ReleaseBoss( _fightPhase );
return;
}
var hp = BossHealth();
if ( !hp.IsValid() ) return;
if ( hp.IsDead || _boss.State == ZombieState.Dead ) { WinBossFight(); return; }
_bossLeft = hp.Current;
SendBossHealth( hp.Fraction );
if ( _fightPhase < 4 && hp.Current <= hp.Max * PhaseLine( _fightPhase ) + 0.5f ) BeginChange( _fightPhase + 1 );
}
/// <summary>
/// The line reached: he cannot be hurt, the zombies round the players burn up, the ground shakes, and the change into the
/// next phase begins — the dive, the arena, the breather, his return (<see cref="RunChange"/>). HOST.
/// </summary>
void BeginChange( int next )
{
var hp = BossHealth();
if ( hp.IsValid() )
{
_bossLeft = hp.Current;
hp.Invulnerable = true;
SendBossHealth( hp.Fraction );
}
var brain = BossBrain();
if ( brain.IsValid() ) brain.Held = true;
BurnAdds();
_bossDiving = _bossBack = _changeAmmo = _changeNav = false;
SetFight( FightChange, next );
NZNet.HexQuake( 2.5f );
Log.Info( $"[nz-hex] ⚔ PHASE {next - 1} IS OVER — Oberon at {_bossLeft:0}/{_bossMax:0}: he dives into the lava, and the arena"
+ $" changes; he is back in {ReturnAt( next ):0}s" );
}
/// <summary>
/// The change run: the dive once he is free of what he was doing — or, still stuck after six seconds, he sinks where he
/// stands; the Max Ammo; the navmesh built again once the arena has changed; and his return, let loose on the new phase once
/// he has come. HOST.
/// </summary>
void RunChange()
{
var t = (float)_fightSince;
if ( !_bossBack && _boss.IsValid() )
{
var brain = BossBrain();
if ( brain.IsValid() && brain.Dived )
{
NZNet.BossFightFx( FxSplash, brain.DivedAt );
_boss.GameObject.Destroy();
_boss = null;
}
else if ( !_bossDiving && brain.IsValid() && brain.Dive( DivePoint( _boss.WorldPosition ) ) )
{
_bossDiving = true;
}
else if ( t > 6f )
{
NZNet.BossFightFx( FxSplash, _boss.WorldPosition );
_boss.GameObject.Destroy();
_boss = null;
}
}
if ( !_changeAmmo && t >= ChangeAmmoAt )
{
_changeAmmo = true;
// ⚠️ ON THE PLATFORM NEAREST THE PLAYERS, not in the ring's middle: into phase 4 the middle is the flood's
var spot = FightHexCentre( NearestFightHex( PlayersMiddle() ) );
var drop = FloorTopNow() + (HexT( _fight, _fightPhase, _fightSince ) > 0f ? FightHexRise : 0f) + 40f;
Powerup.SpawnShared( new Vector3( spot.x, spot.y, drop ), PowerupKind.MaxAmmo );
}
if ( !_changeNav && t >= ChangeDone( _fightPhase ) )
{
_changeNav = true;
ArenaNav();
}
if ( !_bossBack && !_boss.IsValid() && t >= ReturnAt( _fightPhase ) )
{
_boss = SpawnBoss( EmergencePoint() );
_bossBack = true;
}
if ( _bossBack && BossHasCome )
{
ReleaseBoss( _fightPhase );
SetFight( FightPhase, _fightPhase );
}
}
/// <summary>
/// He is dead: every zombie burns up, the step-done clicking, and the green orb floats over his spawn; the flood goes back
/// into its bed on every screen (`HexPlatforms.ArenaHazards.cs`). HOST.
/// </summary>
void WinBossFight()
{
BurnAdds();
SetFight( FightWon, 4 );
SendBossHealth( 0f );
NZNet.BossFightFx( FxCore, OrbAt );
Cue( DoneCue );
Fanfare( 15 );
Log.Info( $"[nz-hex] ✦ OBERON IS DEAD — the green orb floats over his spawn, at {OrbAt:0}: E on it takes everyone home" );
}
/// <summary>E on the orb. LOCAL — the presser's machine asks the host (<see cref="HostTakeCore"/>).</summary>
public static void TakeCore( NZPlayer player )
{
if ( player.IsValid() ) NZNet.BossCoreTake();
}
/// <summary>
/// Someone pressed E on the orb. HOST — `NZNet.BossCoreTake`, `who` as the call carries it, "" the host's own: the passage
/// home begins (<see cref="RunReturn"/>).
/// </summary>
public static void HostTakeCore( string who )
{
if ( NZGame.IsClient ) return;
var m = Instance;
if ( !m.IsValid() || m._fight != FightWon ) return;
var body = CarrierBodyOf( who );
if ( !body.IsValid() && (string.IsNullOrEmpty( who ) || who == Connection.Local?.Id.ToString()) ) body = NZPlayer.Local;
if ( CannotCarry( body ) ) return;
if ( body.WorldPosition.Distance( OrbAt ) > CoreReach + ReachSlack ) { Log.Info( "[nz-hex] the orb is not taken: out of reach" ); return; }
m.SetFight( FightReturn, 4 );
NZSound.PlayShared( "nz.powerup.pickup", OrbAt );
m.Cue( NZSound.TeleporterWarmup, OrbAt );
Log.Info( $"[nz-hex] ✦ THE ORB IS TAKEN — {NameFor( body )} took it: everyone goes home in {ArenaTransitSeconds:0.#}s" );
}
/// <summary>
/// Where the players come home to: a ring of 90u round the blue altar, on the teleporter room's floor, the altar itself kept
/// clear — the teleporter they left by. The i-th of n.
/// </summary>
static Vector3 HomeLanding( int i, int n )
{
var a = MathF.PI * 2f * i / Math.Max( 1, n );
return BlueAltarFoot + new Vector3( MathF.Cos( a ) * 90f, MathF.Sin( a ) * 90f, 2f );
}
/// <summary>
/// The passage home, the orb taken: its arcs on every screen; then everyone — the downed and the dead too, revived — put
/// down round the blue altar, each by its owner, facing it; the rewards; and the Easter egg done, the round loose again.
/// HOST — `WatchBossFight`. ⚠️ While testing no body moves and no one is revived, as for the send.
///
/// ⛔ *"once they win the fight and are teleported back everyone is revived"* — the user's death rule (2026-09-27). The rest
/// of it is already the game's: a player who bleeds out is out until `RoundManager` brings them back as a round clears,
/// and in the frozen fight none does, so they wait for its end; and the last one standing bleeding out ends the game,
/// fight or no fight (`NZPlayer`): *"if all players die they lose the game"*.
/// </summary>
void RunReturn()
{
if ( _fightSince < ArenaTransitSeconds ) return;
var everyone = PlayerSpawner.AllBodies( Scene )
.Select( b => b.Components.Get<NZPlayer>( FindMode.EverythingInSelf ) )
.Where( p => p.IsValid() )
.ToList();
var revived = 0;
if ( !Testing )
{
// ⛔ REVIVED FIRST, PLACED AFTER, as the round's own return does (`RoundManager.BringBackTheBledOut`): a body out of
// the round is out of the world until its owner stands it up. Stood up on the owner's machine — `NZNet.ComeBack` for
// a client's: `Revive` on a proxy is a copy's, and its own relay stands up only the downed, never the dead.
foreach ( var p in everyone )
{
if ( !(p.IsDown || p.DownedNet || p.HasBledOut || p.IsOutOfRound || p.OutOfRoundNet) ) continue;
if ( Networking.IsActive && PlayerPresence.Theirs( p.GameObject ) && NZPlayers.OwnerOf( p.GameObject ) is { Length: > 0 } owner )
NZNet.ComeBack( owner );
else
p.Revive();
revived++;
}
for ( var i = 0; i < everyone.Count; i++ )
{
var at = HomeLanding( i, everyone.Count );
var look = (BlueAltarFoot - at).WithZ( 0f );
PlayerSpawner.MoveTo( everyone[i].GameObject, at, look.Length > 1f ? Rotation.LookAt( look.Normal, Vector3.Up ) : Rotation.Identity );
}
}
Cue( NZSound.TeleporterOut, OrbAt );
Cue( NZSound.TeleporterIn, BlueAltarTop );
GrantEggRewards();
SetFight( FightDone, 4 );
Fanfare( 16 );
Log.Info( $"[nz-hex] ✦✦ BASALT'S EASTER EGG IS COMPLETE — {everyone.Count} player(s) home at the blue altar"
+ (revived > 0 ? $", {revived} of them revived" : "") + "; the round goes on" );
}
/// <summary>
/// The Easter egg's gifts: 50,000 points and 5,000 salvage to everyone in the game, each machine paying its own player
/// (`NZNet.BossFightRewards` → <see cref="ApplyEggRewards"/>). HOST — the Easter egg done (<see cref="RunReturn"/>). The
/// rules among the rewards — Pack-a-Punch to MK6, rarity to Godly, no limit on how many augments a perk holds (not every
/// augment given: the limit lifted, by the user's word) — are not given here: they read <see cref="EggComplete"/> where
/// they are decided. ⚠️ Nothing while testing.
/// </summary>
void GrantEggRewards()
{
if ( Testing ) return;
NZNet.BossFightRewards( EggPoints, EggSalvage );
Log.Info( $"[nz-hex] ✦ the rewards: {EggPoints:N0} points and {EggSalvage:N0} salvage to everyone in the game; from now on"
+ $" Pack-a-Punch to MK{PapSettings.MaxTiers}, rarity to {Rarity.NameFor( Rarity.GodlyTier )}, and no limit on augments" );
}
/// <summary>The Easter egg's gifts to each player: *"50000 points, 5000 salvage"*.</summary>
const int EggPoints = 50000, EggSalvage = 5000;
/// <summary>
/// The Easter egg's gifts, to this machine's own player. EVERY machine — `NZNet.BossFightRewards`: the points through
/// `AddPoints`, the one door every award goes through; the salvage as a gift, whether or not the map pays it for kills.
/// </summary>
public void ApplyEggRewards( int points, int salvage )
{
if ( !OnBasalt ) return;
var me = NZPlayer.Local;
if ( !me.IsValid() ) return;
me.AddPoints( points );
Salvage.Gift( me, salvage );
Log.Info( $"[nz-hex] ✦ the Easter egg's gifts: +{points:N0} points, +{salvage:N0} salvage — {me.Points:N0} and {me.Salvage:N0} now" );
}
/// <summary>
/// The fight over and gone: he and every zombie despawned — with <paramref name="keepAdds"/>, he alone, his zombies left as
/// any zombie, a game over's — the arena as it was, the round loose again. HOST.
/// </summary>
void EndBossFight( string why, bool keepAdds = false )
{
if ( _fight == FightNone ) return;
if ( _boss.IsValid() ) _boss.GameObject.Destroy();
_boss = null;
if ( !keepAdds )
foreach ( var (z, _) in _fightAdds ?? new() )
if ( z.IsValid() && z.GameObject.IsValid() ) z.GameObject.Destroy();
_fightAdds?.Clear();
_bossMax = _bossLeft = 0f;
_bossHealthSent = -1f;
SetFight( FightNone, 0 );
Log.Info( $"[nz-hex] ⚔ the boss fight ends — {why}" );
}
/// <summary>
/// No fight: a new game ends one. HOST — with the send, which it follows. ⚠️ NOT WHILE THE SELFTEST WALKS THE RULES: its new
/// games would end a real fight, and a despawned beast is not a thing it can put back.
/// </summary>
void ResetBossFight()
{
if ( _armedForTest.HasValue ) return;
EndBossFight( "a new game" );
}
/// <summary>
/// A hit on him, as he takes it: a tenth of it — all of it while the Prisma's fuse burns on him (<see cref="BossDamageShare"/>)
/// — and then capped: nothing while he cannot be fought — his coming, a change, the kill done — and in a phase no more than
/// takes him to its line, however hard the hit; in the last, no cap. HOST — `Health.Apply`, where every route in ends:
/// bullets, the knife, the Shrieker's death pulse, a nuke's share.
/// </summary>
public static float BossCap( GameObject victim, float current, float max, float amount )
{
var m = Instance;
if ( !m.IsValid() || m._fight == FightNone || !m._boss.IsValid() || victim != m._boss.GameObject ) return amount;
return BossCapAmount( m._fight, m._fightPhase, current, max, amount * BossDamageShare( BossResonating( victim ) ) );
}
/// <summary>The cap's rule, apart from the fight, for the selftest.</summary>
static float BossCapAmount( int stage, int phase, float current, float max, float amount )
{
if ( stage != FightPhase ) return 0f;
return MathF.Min( amount, MathF.Max( 0f, current - max * PhaseLine( phase ) ) );
}
/// <summary>Is this the fight's boss? HOST — the Shrieker's death pulse spares him (`ShriekerZombie`).</summary>
public static bool IsFightBoss( ZombieAI z ) => z.IsValid() && Instance.IsValid() && Instance._boss == z;
// ══ the mirror ═══════════════════════════════════════════════════════════════════════════
/// <summary>The fight's stage as the host told it. EVERY machine — `NZNet.BossFightState`: the arena drawn to match.</summary>
public void ApplyBossFight( int stage, int phase, float since )
{
var was = _fightShown;
_fightShown = stage;
_fightPhaseShown = phase;
_fightShownSince = since;
if ( stage == FightNone ) { _bossSeenShown = false; _bossHealthShown = 1f; }
// ⚠️ A PHASE OR A CHANGE MEANS HE HAS COME: a joiner, who never saw him come out of the lava, still gets his health bar
if ( stage is FightPhase or FightChange ) _bossSeenShown = true;
if ( stage != was ) _hazardSaid = 0;
// ⛔ NOTHING IS BUILT HERE: `TickHazards` draws the arena from these numbers every frame. A rebuild at each stage would
// take the shrunk floor away for a frame — and whoever stood on it would drop into the lava.
if ( stage == was || Testing || !OnBasalt ) return;
// ⛔ THE EASTER EGG'S RULES OPEN OR SHUT: the guns in this machine's player's hands take what they now count as — an MK6
// or a Godly kept from before is MK5 and Legendary without the egg (`NZPlayer.PapLevelHeld`, `Rarity.TierHeld`), and
// itself again with it. A new game equips its first gun BEFORE the egg is reset, so without this that gun kept the
// last game's MK6 until the next equip.
if ( (was == FightDone) != (stage == FightDone) && NZPlayer.Local is NZPlayer holder && holder.IsValid() )
holder.PushStoredUpgrades();
// the passage home: its arcs on this machine's screen, and its charge heard — everyone rides home, the dead too
if ( stage == FightReturn && NZPlayer.Local is NZPlayer me && me.IsValid() )
{
TeleportOverlayState.Begin( ArenaTransitSeconds + ArenaOverlayLinger );
NZSound.Play( NZSound.TeleporterCharge );
}
// home: a portal round the blue altar, and the news (the rest of it `TickHazards` says, a moment on). ⚠️ The portal only
// as it happens: a joiner, told long after, is not coming home
if ( stage == FightDone )
{
if ( since < 1f ) TeleportPortal.Arrival( BlueAltarFoot );
PowerupBannerState.Show( "THE EASTER EGG IS COMPLETE" );
}
}
/// <summary>His health as the host told it. EVERY machine — `NZNet.BossFightHealth`: the health bar.</summary>
public void ApplyBossHealth( float fraction ) => _bossHealthShown = Math.Clamp( fraction, 0f, 1f );
/// <summary>The fight's moments to see: he comes out of the lava; he dives into it; the orb appears over his spawn.</summary>
const int FxEmerge = 1, FxSplash = 2, FxCore = 3;
/// <summary>
/// One of the fight's moments. EVERY machine — `NZNet.BossFightFx`: the lava bursting where he comes up, with his name on
/// the screen the first time; its splash where he goes under; the core tearing free and the banner.
/// </summary>
public void ApplyBossFx( int kind, Vector3 at )
{
if ( !OnBasalt ) return;
var lava = new Color( 1f, 0.42f, 0.1f );
switch ( kind )
{
case FxEmerge:
ShockRing.Fire( at, 380f, lava );
ShockRing.Fire( at, 190f, lava );
CameraShake.Punch( at, 0.7f, 4000f );
if ( !_bossSeenShown ) PowerupBannerState.Show( "OBERON" );
_bossSeenShown = true;
break;
case FxSplash:
ShockRing.Fire( at, 260f, lava );
CameraShake.Punch( at, 0.4f, 3000f );
NZSound.Play( "nz.oberon.attack", at );
break;
case FxCore:
ShockRing.Fire( at, 500f, OrbGreen );
ShockRing.Fire( at, 250f, OrbGreen );
CameraShake.Punch( at, 0.6f, 4000f );
PowerupBannerState.Show( "THE BEAST IS SLAIN" );
break;
}
}
/// <summary>Is his health bar up, as this machine was told? From his coming to his death.</summary>
public static bool BossBarShown => Instance.IsValid() && OnBasalt && Instance._bossSeenShown
&& Instance._fightShown is FightIntro or FightPhase or FightChange;
/// <summary>His health for the bar, a fraction.</summary>
public static float BossBarFraction => Instance.IsValid() ? Instance._bossHealthShown : 1f;
/// <summary>
/// What the fight asks of the music (`EggMusic`), as this machine was told: on from his coming up to his death — the health bar's
/// span — low through a phase change's breather, and his death. EVERY machine, from the mirror, a joiner mid-fight included.
/// </summary>
public static (bool On, bool Breather, bool Won) FightMusic => !Instance.IsValid() || !OnBasalt
? (false, false, false)
: (BossBarShown, Instance._fightShown == FightChange, Instance._fightShown == FightWon);
/// <summary>The phase for the bar: "PHASE 2" — or where the change leads.</summary>
public static string BossBarPhase => !Instance.IsValid() ? "" : Instance._fightShown == FightChange
? $"PHASE {Instance._fightPhaseShown} IS COMING" : $"PHASE {Math.Max( 1, Instance._fightPhaseShown )}";
/// <summary>His name on the bar.</summary>
public const string BossBarName = "OBERON";
// ══ in words, and by hand ════════════════════════════════════════════════════════════════
/// <summary>The fight, in words. HOST.</summary>
string BossFightText()
{
var hp = BossHealth();
var him = hp.IsValid()
? $"Oberon {hp.Current:0}/{hp.Max:0} ({hp.Fraction * 100f:0}%)"
+ (BossResonating( _boss.GameObject ) ? " — the Prisma's fuse on him: FULL damage" : $" — {BossArmour * 100f:0.#}% of a hit")
: "Oberon not out";
return _fight switch
{
FightNone => $"no boss fight — the blue altar's send starts it; its spawns: {FightSpawns( ActiveConfig.Current?.BossSpawns ).Count}"
+ $" for him, {FightSpawns( ActiveConfig.Current?.ZombieSpawns ).Count} for the rest (flag {FightSpawnFlag})",
FightIntro => $"the boss fight's intro, {(float)_fightSince:0.#}s in — {him}",
FightPhase => $"PHASE {_fightPhase} — {him}; {PhaseText( _fightPhase )}; {_fightAdds?.Count ?? 0} with him",
FightChange => $"the change into phase {_fightPhase}, {(float)_fightSince:0.#}s in — he is back at {ReturnAt( _fightPhase ):0}s — {him}",
FightWon => $"WON — Oberon is dead; the green orb floats over his spawn at {OrbAt:0}: E on it takes everyone home",
FightReturn => "the orb is taken — the passage home",
_ => $"the Easter egg is COMPLETE — its gifts given; from now on Pack-a-Punch to MK{NZPlayer.PapMaxLevel}, rarity to"
+ $" {Rarity.NameFor( Rarity.TopTier )}, augments {(PerkAugments.Unlimited ? "unlimited" : "LIMITED")}; the round goes on",
};
}
/// <summary>
/// `nz_hex_boss [start|next|phase N|kill|done|end|hp X|armour X|geom]` — HOST: the boss fight. Bare, where it stands. `start` begins it here
/// and now — every zombie despawned, the round frozen — without the send (`nz_hex_blue_altar use` sends too); `next` takes
/// him to his phase's line, so the change into the next begins; `phase N` jumps to phase N's change; `kill` kills him;
/// `done` completes the Easter egg here and now, fight or none — its gifts given, its rules open, nobody moved; `end` ends
/// the fight and puts the arena back — the Easter egg undone too; `hp X` sets his pool, in walkers' health for each player
/// (<see cref="BossHealthMultiple"/>), from his next first coming; `armour X` what he takes of a hit out of the Prisma's
/// fuse (<see cref="BossArmour"/>); `geom` reports the arena's shapes.
/// </summary>
[ConCmd( "nz_hex_boss" )]
public static void BossCmd( string what = "", float value = 0f )
{
if ( NZGame.IsClient ) { Log.Warning( "[nz-hex] host only" ); return; }
var m = Instance;
if ( !m.IsValid() || !OnBasalt ) { Log.Info( "[nz-hex] the boss fight is basalt's" ); return; }
switch ( what.Trim().ToLowerInvariant() )
{
case "":
break;
case "start":
if ( m._fight != FightNone ) { Log.Warning( "[nz-hex] the boss fight is on already — nz_hex_boss end first" ); return; }
m.StartBossFight();
break;
case "next":
{
var hp = m.BossHealth();
if ( m._fight != FightPhase || !hp.IsValid() || !m._bossReleased ) { Log.Warning( "[nz-hex] not in a phase with him out" ); return; }
if ( m._fightPhase >= 4 ) { Log.Warning( "[nz-hex] the last phase — nz_hex_boss kill" ); return; }
hp.SetCurrent( hp.Max * PhaseLine( m._fightPhase ) );
break;
}
case "phase":
{
var n = Math.Clamp( (int)value, 1, 4 );
if ( m._fight == FightNone ) m.StartBossFight();
if ( n == 1 ) break;
if ( m._bossMax <= 0f && m.BossHealth() is Health first && first.Max > 0f ) m._bossMax = first.Max;
m.BeginChange( n );
// ⚠️ AFTER THE CHANGE BEGINS, which takes what he has left as he stands: he comes back with phase N's share
if ( m._bossMax > 0f ) m._bossLeft = m._bossMax * PhaseLine( n - 1 );
else m._bossFirstFraction = PhaseLine( n - 1 );
break;
}
case "kill":
{
var hp = m.BossHealth();
if ( !hp.IsValid() || m._fight != FightPhase ) { Log.Warning( "[nz-hex] not in a phase with him out" ); return; }
m._fightPhase = 4;
hp.Invulnerable = false;
hp.Apply( hp.Max * 10f, false, null );
break;
}
case "done":
if ( m._fight == FightDone ) { Log.Warning( "[nz-hex] the Easter egg is complete already — nz_hex_boss end undoes it" ); return; }
if ( m._fight != FightNone ) m.EndBossFight( "by hand, for the Easter egg's end" );
m.GrantEggRewards();
m.SetFight( FightDone, 4 );
Log.Info( "[nz-hex] ✦✦ BASALT'S EASTER EGG IS COMPLETE — by hand" );
break;
case "end":
m.EndBossFight( "by hand" );
break;
case "hp":
if ( value > 0f ) BossHealthMultiple = value;
Log.Info( $"[nz-hex] Oberon's pool: {BossHealthMultiple:0.##} walkers' health for the round, for each of the"
+ $" {m.PlayersInGame()} player(s) — {m.BossPool():N0} at this round; from his next first coming" );
break;
case "armour":
if ( value > 0f ) BossArmour = value;
Log.Info( $"[nz-hex] Oberon takes {BossArmour * 100f:0.#}% of a hit — and all of it while the Prisma's fuse burns on him" );
break;
case "geom":
GeometryReport();
return;
default:
Log.Warning( "[nz-hex] nz_hex_boss start, next, phase N, kill, done, end, hp X, armour X or geom — or nothing, to see where it stands" );
return;
}
Log.Info( $"[nz-hex] {m.BossFightText()}" );
}
/// <summary>
/// `nz_hex_boss geom` — the arena's shapes as this build has them: the shrunk floor's points, the first pair of its edges
/// that cross, if any, and every platform corner that falls outside it. Written for the selftest's geometry checks, which
/// can say only that one failed.
/// </summary>
static void GeometryReport()
{
var p = ArenaShrunkOutline;
Log.Info( $"[nz-hex] the shrunk floor: {p.Length} points, from {p[0]} to {p[^1]}; the upper floor's {ArenaStageOutline.Length}" );
static float Cross( Vector2 a, Vector2 b ) => (a.x * b.y) - (a.y * b.x);
var crossings = 0;
for ( var i = 0; i < p.Length; i++ )
{
Vector2 a1 = p[i], a2 = p[(i + 1) % p.Length];
for ( var j = i + 1; j < p.Length; j++ )
{
if ( (j + 1) % p.Length == i || j == (i + 1) % p.Length ) continue;
Vector2 b1 = p[j], b2 = p[(j + 1) % p.Length];
float d1 = Cross( b2 - b1, a1 - b1 ), d2 = Cross( b2 - b1, a2 - b1 ), d3 = Cross( a2 - a1, b1 - a1 ), d4 = Cross( a2 - a1, b2 - a1 );
if ( ((d1 > 0f) != (d2 > 0f)) && ((d3 > 0f) != (d4 > 0f)) )
{
if ( crossings++ < 4 )
Log.Info( $"[nz-hex] edges {i} and {j} cross: {a1}→{a2} and {b1}→{b2} (d {d1:0.####} {d2:0.####} {d3:0.####} {d4:0.####})" );
}
}
}
Log.Info( $"[nz-hex] {crossings} crossing(s)" );
float a = Apothem, r = Apothem * 2f / MathF.Sqrt( 3f );
var corners = new[] { new Vector2( 0f, r ), new Vector2( -a, r * 0.5f ), new Vector2( -a, -r * 0.5f ),
new Vector2( 0f, -r ), new Vector2( a, -r * 0.5f ), new Vector2( a, r * 0.5f ) };
for ( var k = 0; k < FightHexes; k++ )
foreach ( var d in corners )
{
var c = FightHexCentre( k ) + d;
if ( !InOutline( p, c ) ) Log.Info( $"[nz-hex] platform {k + 1}'s corner {c} is outside the shrunk floor" );
}
}
}