Game/Globals.cs

Global constants and helpers for the game. Defines Direction and TurnMode enums, many render depth and particle gravity constants, a DepthToZ helper, cached direction list and utility functions mapping directions to vectors, rotations, opposites, random choice and strings.

Native Interop
using System.Collections.Generic;

namespace BlockParty;

public enum Direction { None, Left, Right, Up, Down }

/// <summary>Per-spawn-slot rule for how a block re-picks its direction after stopping.
/// <see cref="Free"/> = the classic random re-pick (may reverse). <see cref="Always"/> = take an open
/// perpendicular direction instead of reversing. <see cref="Clockwise"/>/<see cref="CounterClockwise"/>
/// = a track cycler: keep the current heading whenever it's open, wait (pressing forward) while a block
/// ahead occupies it, and rotate one step CW/CCW only when the heading is walled off — so a conga of
/// blocks patrols a loop without ever backing up. The cycle pick replaces the subtype's own preferred
/// pick (a cycling Hunter patrols instead of re-aiming); subtypes that don't move by MoveDirection
/// (Wisp's free float) aren't meaningfully affected.</summary>
public enum TurnMode { Free, Always, Clockwise, CounterClockwise }

/// <summary>
/// Port of the original <c>Globals</c> manager: direction helpers, per-entity-type
/// depth constants, and particle gravity strengths.
///
/// Depth constants are integer "layers" exactly as in the original (higher = nearer
/// the camera). They are converted to a world-space Z by <see cref="DepthToZ"/>.
/// </summary>
public static class Globals
{
	// Render depth layers (higher = drawn in front). Matches the original.
	public const int DEPTH_TEXT = 999;
	public const int DEPTH_MIMIC_CHOICE = 14;
	public const int DEPTH_PORTAL_HIGHLIGHT = 13; // the portal's smaller light-purple inner square: its own integer layer above the dark square (sub-integer Z offsets z-fight under the ortho camera — see SpriteLayer)
	public const int DEPTH_PORTAL = 12;          // wrap-portal flash: over EVERYTHING but text (walls, blockers, player, blocks), so the portal reads even where the wrap point is buried in a wall / out of bounds
	public const float DEPTH_VISION_COVER = 11.5f; // line-of-sight cover: the opaque shadow cast by vision-blocking rects from the player's position. ABOVE the walls so it also obscures wall spikes behind a blocker (the geometry is clipped to the arena so it never bleeds into the pillarbox); below the portal flash + HUD text. See VisionOccluder. Sits half a layer up (not on 11) so it can't share a Z with the piercing teardrop below (the coin floater's 11.25 splits the gap, clearing both by 0.25).
	public const float DEPTH_COIN_FLOATER = 11.25f; // the coin's rising "+N" popup: score feedback that must never be buried — over walls/obstacles (10-10.9), players, blocks and every hazard (piercing teardrop = 11), but still UNDER the fog-of-war cover (it spawns at the player, the vision origin, so the cover never actually hides it) and the portal flash/text
	public const int DEPTH_ABILITY_INDICATOR = DEPTH_PORTAL; // small player ability tells: over blocks, obstacles, arena walls, and ordinary hazards
	public const int DEPTH_PIERCING_TEARDROP = 11; // red tears pass visibly over obstacle faces and the arena wall's 10.7-10.9 layers, but stay UNDER the vision cover — a hazard hidden by the fog-of-war shadow like every other one
	public const float DEPTH_ARENA_WALL = 10.7f; // outer arena wall root; its 0.1/0.2 band/teeth children remain below vision cover
	public const float DEPTH_BLOCKER = 10.65f;   // OOB masks: above piercing lasers, just behind the outer arena walls
	public const int DEPTH_WALL = 10;            // interior obstacle fill/face root
	public const int DEPTH_PARTICLE_1 = 5;
	public const int DEPTH_FIREBALL = 4;
	public const int DEPTH_TEARDROP = 4;
	public const int DEPTH_SUMMON_PORTAL = 4;   // Summoner tell + motes: above player bodies, below the high particle plane
	public const int DEPTH_PLAYER = 3;
	public const int DEPTH_BLOCK = 2;
	public const float DEPTH_COIN = 1.75f;       // coin pickups: BEHIND blocks/player/hazards (everything gameplay passes in front), above glass so a coin still reads on a tinted pane. 1.75 (not 1.7) on purpose: glass stacks childOrder * 0.1 sub-layers on DEPTH_GLASS (fill 1.6, streaks 1.7, border 1.8, teeth 1.9 — see GameStage.GLASS_ORDER_*), so the body (+0) lands at 1.75 between streaks and border, and the sparkle child (+0.1) at 1.85 between border and teeth — no shared Z to flicker against. 0.05 separation is enough (same margin as DEPTH_BLOCKER vs DEPTH_WALL's band).
	public const float DEPTH_GLASS = 1.5f;       // glass panels: above fences, still UNDER blocks/player/hazards — blocks and shots visibly slide over the tinted pane (only the player collides)
	public const int DEPTH_FENCE = 1;            // fence frame/bars: over the playfield + low particles, UNDER blocks/player/hazards — everything that ignores a fence visibly passes in front of it
	public const int DEPTH_PARTICLE_0 = 0;
	public const int DEPTH_LANE_OVERLAY = -1;    // Wind/Magnet lane tint: just below the low particle layer, above the background. Per-block Z sub-offset (via DepthToZ's childOrder = block StageIndex) keeps overlapping lanes from different blocks from z-fighting.
	public const int DEPTH_BACKGROUND_PARTICLE = -3; // per-level falling-square ambience (rain/snow/embers): over the background hints and drifting cosmetic blocks, under every gameplay layer — blocks, glass and especially fences visibly pass IN FRONT of the weather
	public const int DEPTH_BACKGROUND_INFO = -5; // per-level persistent background hints (tutorial MOVE + arrow keys): in front of the drifting cosmetic blocks so they stay readable, behind every gameplay layer
	public const int DEPTH_BACKGROUND = -10;     // nearest cosmetic-block layer; deeper layers step back from here
	public const int DEPTH_PLAYFIELD = -40;      // the static teal play-area square, behind all background blocks
	public const int DEPTH_BACKDROP = -39;       // the dark fade-colour backdrop for "covered" stages (score / leaderboard): in front of the playfield, behind all stage content

	// Particle gravity strengths.
	public const float GRAVITY_STR_DUST = 100.0f;
	public const float GRAVITY_STR_BLOOD = -150.0f;
	public const float GRAVITY_STR_GLITTER = 400.0f;

	/// <summary>
	/// Converts a logical depth layer (+ a small per-child offset) into a world Z.
	/// The 2D play-plane is XY; the camera looks down -Z, so larger Z is nearer.
	/// Takes a float so the fractional layers (arena wall, blockers, vision cover) go through the
	/// same conversion as the integer ones; every int caller still binds via implicit conversion.
	/// </summary>
	public static float DepthToZ( float depth, int childOrder = 0 )
	{
		return depth + childOrder * 0.01f;
	}

	// Shared cached instance: this is iterated several times per block per tick (side/sticky/shake
	// upkeep, enclosure probes), and returning a fresh List here allocated on every one of those.
	static readonly Direction[] AllDirections = { Direction.Left, Direction.Right, Direction.Up, Direction.Down };

	/// <summary>The four real directions. A read-only span over the shared cached array: handing it
	/// out is allocation-free AND structurally immutable — a caller can't write through it or pass it
	/// to something that would (e.g. IList's Shuffle), so the shared order can never be corrupted.</summary>
	public static ReadOnlySpan<Direction> GetAllDirections() => AllDirections;

	public static Vector2 GetVectorForDirection( Direction dir )
	{
		if ( dir == Direction.Left ) return new Vector2( -1.0f, 0.0f );
		else if ( dir == Direction.Right ) return new Vector2( 1.0f, 0.0f );
		else if ( dir == Direction.Down ) return new Vector2( 0.0f, -1.0f );
		else if ( dir == Direction.Up ) return new Vector2( 0.0f, 1.0f );
		return Vector2.Zero;
	}

	public static Direction GetClockwiseDirection( Direction dir )
	{
		if ( dir == Direction.Left ) return Direction.Up;
		else if ( dir == Direction.Right ) return Direction.Down;
		else if ( dir == Direction.Down ) return Direction.Left;
		else if ( dir == Direction.Up ) return Direction.Right;
		return Direction.None;
	}

	public static Direction GetCounterClockwiseDirection( Direction dir )
	{
		if ( dir == Direction.Left ) return Direction.Down;
		else if ( dir == Direction.Right ) return Direction.Up;
		else if ( dir == Direction.Down ) return Direction.Right;
		else if ( dir == Direction.Up ) return Direction.Left;
		return Direction.None;
	}

	public static Direction GetOppositeDirection( Direction dir )
	{
		if ( dir == Direction.Left ) return Direction.Right;
		else if ( dir == Direction.Right ) return Direction.Left;
		else if ( dir == Direction.Down ) return Direction.Up;
		else if ( dir == Direction.Up ) return Direction.Down;
		return Direction.None;
	}

	public static Direction GetRandomDirection()
		=> AllDirections[Rng.Int( 0, AllDirections.Length )];

	public static string GetStringForDirection( Direction dir )
	{
		if ( dir == Direction.Left ) return "left";
		else if ( dir == Direction.Right ) return "right";
		else if ( dir == Direction.Down ) return "down";
		else if ( dir == Direction.Up ) return "up";
		return "";
	}
}