Files
Project-M/Assets/_Project/Scripts/Simulation/World/RunMap.cs
T
2026-07-02 20:41:43 -07:00

109 lines
5.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using Unity.Collections;
namespace ProjectM.Simulation
{
/// <summary>
/// Room-TYPE ids for a run-map node. A <c>byte</c>, never a C# enum — kept Burst-safe, serialization/replay-stable,
/// and APPEND-ONLY (a persisted meta / replay reproducibility depends on these values never being re-meaned).
/// </summary>
public static class RoomTypeId
{
public const byte Combat = 0;
public const byte Elite = 1;
public const byte Reward = 2;
public const byte Boss = 3;
public const byte Count = 4;
}
/// <summary>
/// Room SHAPE ids — the arena footprint <see cref="RoomLayoutMath.ScatterInShape"/> places nodes/enemies within.
/// A <c>byte</c> (append-only). Resolved to a concrete radius/footprint by <see cref="RoomLayoutMath"/>.
/// </summary>
public static class RoomShapeId
{
public const byte Disk = 0; // circular arena (area-uniform scatter)
public const byte Wide = 1; // rectangle, wider on X
public const byte Long = 2; // rectangle, longer on Z
public const byte Cross = 3; // plus/cross of two bars
public const byte Count = 4;
}
/// <summary>
/// Cosmetic BIOME ids — resolved to atmosphere/fog/tint by the client presentation layer (WorldAtmosphereSystem)
/// per room. A <c>byte</c> (append-only); purely visual, no gameplay authority.
/// </summary>
public static class RoomBiomeId
{
public const byte Meadow = 0;
public const byte Arid = 1;
public const byte Cavern = 2;
public const byte Blight = 3;
public const byte Count = 4;
}
/// <summary>
/// One node in the branching run-map DAG (Slay-the-Spire style). 4 bytes, unmanaged. <see cref="NextMask"/> is a
/// bit set: bit <c>j</c> ⇒ this node can advance to column <c>j</c> of the NEXT layer (<c>j &lt; RunMap.MaxWidth</c>).
/// A node with <see cref="NextMask"/> == 0 is a terminal (the single Boss node). Generated purely from the run seed
/// by <see cref="RunMapMath.Generate"/>, so it is identical on server + client (client regenerates for display).
/// </summary>
public struct RunMapNode : IEquatable<RunMapNode>
{
/// <summary><see cref="RoomTypeId"/>.</summary>
public byte RoomType;
/// <summary><see cref="RoomBiomeId"/> (cosmetic).</summary>
public byte Biome;
/// <summary><see cref="RoomShapeId"/>.</summary>
public byte ShapeId;
/// <summary>Reachable next-layer columns: bit <c>j</c> ⇒ column <c>j</c> of the next layer. 0 = terminal (Boss).</summary>
public byte NextMask;
public bool Equals(RunMapNode o) =>
RoomType == o.RoomType && Biome == o.Biome && ShapeId == o.ShapeId && NextMask == o.NextMask;
public override bool Equals(object o) => o is RunMapNode n && Equals(n);
public override int GetHashCode() => RoomType | (Biome << 8) | (ShapeId << 16) | (NextMask << 24);
}
/// <summary>
/// A generated branching run map: a layered DAG the party traverses one node per layer. TRANSIENT — regenerated
/// from the run seed via <see cref="RunMapMath.Generate"/> and NEVER a ghost buffer / never persisted (only the
/// seed + the party's current column ride the wire). Fixed stride of <see cref="MaxWidth"/> per layer, so the
/// stable node key <c>nodeId = layer*MaxWidth + col</c> resolves the SAME room regardless of the path taken —
/// which keeps per-room content (layout, boons) deterministic. Bounded to <see cref="MaxNodes"/> so it lives in a
/// <see cref="FixedList512Bytes{T}"/> (30 × 4 B = 120 B).
/// </summary>
public struct RunMap
{
/// <summary>Max layers (run length is seed-varied within [6, <see cref="MaxLayers"/>]).</summary>
public const int MaxLayers = 10;
/// <summary>Max nodes per layer (branch width 13).</summary>
public const int MaxWidth = 3;
/// <summary>Node-buffer capacity (fixed stride): <see cref="MaxLayers"/> × <see cref="MaxWidth"/>.</summary>
public const int MaxNodes = MaxLayers * MaxWidth;
/// <summary>Nodes, fixed stride <see cref="MaxWidth"/> per layer (<c>LayerCount*MaxWidth</c> entries; columns
/// ≥ <see cref="Width"/> are absent/unused).</summary>
public FixedList512Bytes<RunMapNode> Nodes;
/// <summary>Per-layer branch width (<see cref="LayerCount"/> entries, each in [1, <see cref="MaxWidth"/>]).</summary>
public FixedList64Bytes<byte> LayerWidths;
/// <summary>Number of layers this run (== room count, in [6, <see cref="MaxLayers"/>]).</summary>
public byte LayerCount;
/// <summary>Stable node key for a (layer, col) — fixed stride, so the same key is the same room on any path.</summary>
public static int NodeId(int layer, int col) => layer * MaxWidth + col;
/// <summary>Branch width of a layer.</summary>
public int Width(int layer) => LayerWidths[layer];
/// <summary>Node at (layer, col).</summary>
public RunMapNode Node(int layer, int col) => Nodes[NodeId(layer, col)];
/// <summary>Node by stable id.</summary>
public RunMapNode NodeAt(int nodeId) => Nodes[nodeId];
/// <summary>Layer of a node id.</summary>
public int LayerOf(int nodeId) => nodeId / MaxWidth;
/// <summary>Column of a node id.</summary>
public int ColOf(int nodeId) => nodeId % MaxWidth;
/// <summary>The single Boss node id (last layer, column 0).</summary>
public int BossNodeId => NodeId(LayerCount - 1, 0);
}
}