109 lines
5.6 KiB
C#
109 lines
5.6 KiB
C#
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 < 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 1–3).</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);
|
||
}
|
||
}
|