Data model types for the SboxDirector system, including enums, lightweight vector, snapshots of participants/world/population, spawn candidates and requests, decision batches, events, diagnostics, and validation logic for world resources.
#nullable enable annotations
namespace SboxDirector;
public enum TempoPhase { BuildUp, SustainPeak, PeakFade, Relax }
public enum PressureSeverity { Minor = 1, Moderate = 2, Major = 3, Critical = 4, Maximum = 5 }
public enum EncounterKind { Ambient, CommonWave, Special, Boss, DormantHazard, Objective, Custom }
public enum RequestResultKind { Succeeded, PartiallySucceeded, Rejected, Deferred, Failed }
public enum WaveState { Inactive, InitialDelay, IssueWave, WaitForClear, Pause, Done }
public enum ScenarioStatus { Inactive, Running, Complete, Failed }
public enum AdvanceRelation { Unknown, Ahead, Behind, Lateral, ObjectiveAdjacent }
public enum PlacementFallbackMode { None, IgnoreRouteConstraints, AnyReachableHidden }
public enum PlacementSelectionMode { WeightedScore, FirstEligible, HighestProgressFirstTie, NativeRandomTranspositionFirst }
public readonly record struct DirectorVector(float X, float Y, float Z)
{
public static float DistanceSquared(DirectorVector a, DirectorVector b)
{
var x = a.X - b.X; var y = a.Y - b.Y; var z = a.Z - b.Z;
return x * x + y * y + z * z;
}
}
public sealed class ParticipantSnapshot
{
public string Id { get; init; } = "";
public bool IsAlive { get; init; } = true;
public bool IsEligible { get; init; } = true;
public bool IsInCombat { get; init; }
public DirectorVector Position { get; init; }
public float Progress { get; init; }
}
public sealed class PopulationSnapshot
{
readonly Dictionary<EncounterKind, int> _counts = new();
public PopulationSnapshot() { }
public PopulationSnapshot(IReadOnlyDictionary<EncounterKind, int> counts) { foreach (var pair in counts) _counts[pair.Key] = Math.Max(0, pair.Value); }
public int this[EncounterKind kind] => _counts.TryGetValue(kind, out var value) ? value : 0;
public IReadOnlyDictionary<EncounterKind, int> Counts => _counts;
}
public sealed class SpawnCandidate
{
public string Id { get; init; } = "";
public DirectorVector Position { get; init; }
public bool Reachable { get; init; }
public bool Spawnable { get; init; }
public bool Visible { get; init; }
public float NearestParticipantDistance { get; init; }
public float Progress { get; init; }
public float AreaCapacity { get; init; } = 1f;
public float ThreatSeparation { get; init; }
public float RelativeProgress { get; init; }
public float PathDistance { get; init; }
public float IngressDistance { get; init; }
public float ClearRadius { get; init; }
public float AreaWidth { get; init; }
public float AreaHeight { get; init; }
public AdvanceRelation AdvanceRelation { get; init; }
public IReadOnlySet<string> Tags { get; init; } = new HashSet<string>();
}
public sealed class WorldSnapshot
{
public double Time { get; init; }
public IReadOnlyList<ParticipantSnapshot> Participants { get; init; } = Array.Empty<ParticipantSnapshot>();
public PopulationSnapshot Population { get; init; } = new();
public IReadOnlyList<SpawnCandidate> SpawnCandidates { get; init; } = Array.Empty<SpawnCandidate>();
public bool CombatActive { get; init; }
public float TeamProgress { get; init; }
public string ModeId { get; init; } = "continuous";
public string? RoundId { get; init; }
public ResourceWorldSnapshot Resources { get; init; } = new();
public void Validate()
{
if (double.IsNaN(Time) || double.IsInfinity(Time)) throw new ArgumentException("World time must be finite.");
if (float.IsNaN(TeamProgress) || float.IsInfinity(TeamProgress)) throw new ArgumentException("Team progress must be finite.");
if (Participants.Any(x => string.IsNullOrWhiteSpace(x.Id)) || Participants.Select(x => x.Id).Distinct(StringComparer.Ordinal).Count() != Participants.Count) throw new ArgumentException("Participant ids must be non-empty and unique.");
if (SpawnCandidates.Any(x => string.IsNullOrWhiteSpace(x.Id)) || SpawnCandidates.Select(x => x.Id).Distinct(StringComparer.Ordinal).Count() != SpawnCandidates.Count) throw new ArgumentException("Spawn candidate ids must be non-empty and unique.");
if (SpawnCandidates.Any(x => !float.IsFinite(x.NearestParticipantDistance) || !float.IsFinite(x.Progress) || !float.IsFinite(x.RelativeProgress) || !float.IsFinite(x.AreaCapacity) || !float.IsFinite(x.ThreatSeparation) || !float.IsFinite(x.PathDistance) || !float.IsFinite(x.IngressDistance) || !float.IsFinite(x.ClearRadius) || !float.IsFinite(x.AreaWidth) || !float.IsFinite(x.AreaHeight) || x.NearestParticipantDistance < 0 || x.AreaCapacity < 0 || x.ThreatSeparation < 0 || x.PathDistance < 0 || x.IngressDistance < 0 || x.ClearRadius < 0 || x.AreaWidth < 0 || x.AreaHeight < 0)) throw new ArgumentException("Spawn candidate distances, dimensions, and capacity must be finite and non-negative.");
Resources.Validate();
}
}
public sealed record EncounterMember(string Archetype, int Count);
public sealed record SpawnRequest(
long RequestId,
EncounterKind Kind,
int RequestedCount,
string CandidateId,
string Archetype,
string Reason)
{
public IReadOnlyList<EncounterMember> Composition { get; init; } = Array.Empty<EncounterMember>();
public int Attempt { get; init; } = 1;
public bool Equals(SpawnRequest? other) => other is not null && RequestId == other.RequestId && Kind == other.Kind && RequestedCount == other.RequestedCount && CandidateId == other.CandidateId && Archetype == other.Archetype && Reason == other.Reason && Attempt == other.Attempt && Composition.SequenceEqual(other.Composition);
public override int GetHashCode()
{
var hash = HashCode.Combine(RequestId, Kind, RequestedCount, CandidateId, Archetype, Reason, Attempt); foreach (var member in Composition) hash = HashCode.Combine(hash, member); return hash;
}
}
public sealed record SpawnRequestResult(
long RequestId,
RequestResultKind Result,
int SpawnedCount,
string? Detail = null);
public sealed record DirectorEvent(string Name, string Reason, double Time);
public sealed class DirectorDecisionBatch
{
public double Time { get; init; }
public TempoPhase Tempo { get; init; }
public float TeamPressure { get; init; }
public float MusicIntensity { get; init; }
public List<SpawnRequest> SpawnRequests { get; } = new();
public List<ResourceSpawnRequest> ResourceRequests { get; } = new();
public List<DirectorEvent> Events { get; } = new();
}
public sealed record PressureEvent(string ParticipantId, PressureSeverity Severity, string Reason);
public sealed record DirectorDiagnostic(double Time, string Category, string Message);