Unify junction props

This commit is contained in:
2026-05-10 18:05:32 +02:00
parent 1aa9734e08
commit 9cd9defc0b
13 changed files with 70 additions and 84 deletions

View File

@@ -19,6 +19,24 @@ public abstract class Balancing
return value >= caution ? EBand.Caution : EBand.Safe;
}
public IReadOnlyList<JunctionRatioPreset> JunctionRatios(int outflowCount)
{
return outflowCount switch {
2 => TwoOutflowJunctionRatios,
3 => ThreeOutflowJunctionRatios,
_ => Array.Empty<JunctionRatioPreset>()
};
}
public float[] JunctionWeights(int outflowCount, int mode)
{
var ratios = JunctionRatios(outflowCount);
if (ratios.Count == 0)
return Array.Empty<float>();
return ratios[Math.Clamp(mode, 0, ratios.Count - 1)].Weights;
}
public abstract int DefaultLevelWidth { get; }
public abstract int DefaultLevelHeight { get; }
public abstract int MinimumLevelSize { get; }
@@ -47,6 +65,8 @@ public abstract class Balancing
public abstract float SourceIntensity { get; }
public abstract float DistanceAmountFalloff { get; }
public abstract float DistanceIntensityFalloff { get; }
public abstract IReadOnlyList<JunctionRatioPreset> TwoOutflowJunctionRatios { get; }
public abstract IReadOnlyList<JunctionRatioPreset> ThreeOutflowJunctionRatios { get; }
public abstract float ConsumerRequiredAmount { get; }
public abstract float ConsumerRequiredIntensity { get; }
public abstract float LeakBaseAmount { get; }

View File

@@ -30,6 +30,19 @@ public class NormalBalancing : Balancing
public override float SourceIntensity => 8;
public override float DistanceAmountFalloff => 0.5f;
public override float DistanceIntensityFalloff => 0.4f;
public override IReadOnlyList<JunctionRatioPreset> TwoOutflowJunctionRatios { get; } = [
new("0/4", [0, 1]),
new("1/3", [0.25f, 0.75f]),
new("2/2", [0.5f, 0.5f]),
new("3/1", [0.75f, 0.25f]),
new("4/0", [1, 0])
];
public override IReadOnlyList<JunctionRatioPreset> ThreeOutflowJunctionRatios { get; } = [
new("0/3/3", [0, 0.5f, 0.5f]),
new("3/0/3", [0.5f, 0, 0.5f]),
new("3/3/0", [0.5f, 0.5f, 0]),
new("2/2/2", [1f / 3f, 1f / 3f, 1f / 3f])
];
public override float ConsumerRequiredAmount => 2.5f;
public override float ConsumerRequiredIntensity => 2.5f;
public override float LeakBaseAmount => 0.5f;

View File

@@ -17,9 +17,7 @@ public sealed record JunctionFlow
if (index < 0)
return 0;
var weights = Prop.Type == EPropType.TJunction
? TJunctionWeights(Prop.TJunctionMode)
: CrossJunctionWeights(Prop.CrossJunctionMode);
var weights = Balancing.Current.JunctionWeights(OutgoingBranches.Count, Prop.JunctionMode);
return index < weights.Length ? weights[index] : 0;
}
@@ -33,27 +31,4 @@ public sealed record JunctionFlow
return -1;
}
private static float[] TJunctionWeights(ETJunctionMode mode)
{
return mode switch {
ETJunctionMode.ZeroFour => [0, 1],
ETJunctionMode.OneThree => [0.25f, 0.75f],
ETJunctionMode.TwoTwo => [0.5f, 0.5f],
ETJunctionMode.ThreeOne => [0.75f, 0.25f],
ETJunctionMode.FourZero => [1, 0],
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unsupported T-junction mode.")
};
}
private static float[] CrossJunctionWeights(ECrossJunctionMode mode)
{
return mode switch {
ECrossJunctionMode.ZeroThreeThree => [0, 0.5f, 0.5f],
ECrossJunctionMode.ThreeZeroThree => [0.5f, 0, 0.5f],
ECrossJunctionMode.ThreeThreeZero => [0.5f, 0.5f, 0],
ECrossJunctionMode.TwoTwoTwo => [1f / 3f, 1f / 3f, 1f / 3f],
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unsupported cross-junction mode.")
};
}
}

View File

@@ -12,7 +12,7 @@ public static class JunctionFlowAnalyzer
{
var position = new GridPosition(x, y);
var prop = level.GetProp(position);
if (prop.Type is not (EPropType.TJunction or EPropType.CrossJunction))
if (prop.Type != EPropType.Junction)
continue;
flows.Add(AnalyzeJunction(level, position, prop));
@@ -34,9 +34,8 @@ public static class JunctionFlowAnalyzer
? position.Neighbors().Where(level.InBounds).Where(neighbor => level.GetUnderground(neighbor, carrier).CarriesFlow).ToArray()
: Array.Empty<GridPosition>();
var expectedBranches = prop.Type == EPropType.TJunction ? 3 : 4;
if (carriers.Length == 1 && branches.Length != expectedBranches)
errors.Add($"{prop.Type} must have exactly {expectedBranches} connected underground branches.");
if (carriers.Length == 1 && branches.Length is not 3 and not 4)
errors.Add("Junction must have one incoming branch and two or three outgoing branches.");
var sourceBranches = carriers.Length == 1
? branches.Select(branch => new SourceBranch(branch, ShortestDistanceToSource(level, branch, position, carrier)))

View File

@@ -0,0 +1,3 @@
namespace ReactorMaintenance.Simulation;
public sealed record JunctionRatioPreset(string Label, float[] Weights);

View File

@@ -14,8 +14,7 @@ public enum EEditorTool
FuelConsumer,
CoolantConsumer,
ElectricityConsumer,
TJunction,
CrossJunction,
Junction,
Door,
AllSeeingEyeTerminal,
FuelRemedySupply,
@@ -53,8 +52,7 @@ public static class LevelEditor
EEditorTool.FuelConsumer => SetCarrierProp(level, position, EPropType.Consumer, ECarrierType.Fuel),
EEditorTool.CoolantConsumer => SetCarrierProp(level, position, EPropType.Consumer, ECarrierType.Coolant),
EEditorTool.ElectricityConsumer => SetCarrierProp(level, position, EPropType.Consumer, ECarrierType.Electricity),
EEditorTool.TJunction => SetFloorProp(level, position, new() { Type = EPropType.TJunction }),
EEditorTool.CrossJunction => SetFloorProp(level, position, new() { Type = EPropType.CrossJunction }),
EEditorTool.Junction => SetFloorProp(level, position, new() { Type = EPropType.Junction }),
EEditorTool.Door => SetDoor(level, position),
EEditorTool.AllSeeingEyeTerminal => SetFloorProp(level, position, new() { Type = EPropType.AllSeeingEyeTerminal }),
EEditorTool.FuelRemedySupply => SetFloorProp(level, position, new() { Type = EPropType.RemedySupply, RemedyType = ERemedyType.FuelNeutralizer }),

View File

@@ -1,9 +0,0 @@
namespace ReactorMaintenance.Simulation;
public enum ECrossJunctionMode
{
ZeroThreeThree,
ThreeZeroThree,
ThreeThreeZero,
TwoTwoTwo
}

View File

@@ -5,8 +5,7 @@ public enum EPropType
None,
Flow,
Consumer,
TJunction,
CrossJunction,
Junction,
Door,
AllSeeingEyeTerminal,
RemedySupply,

View File

@@ -1,10 +0,0 @@
namespace ReactorMaintenance.Simulation;
public enum ETJunctionMode
{
ZeroFour,
OneThree,
TwoTwo,
ThreeOne,
FourZero
}

View File

@@ -6,8 +6,7 @@ public sealed record PropState
public ECarrierType Carrier { get; init; }
public EPropSwitchState SwitchState { get; init; } = EPropSwitchState.Enabled;
public EConsumerServiceState ServiceState { get; init; } = EConsumerServiceState.Unknown;
public ETJunctionMode TJunctionMode { get; init; } = ETJunctionMode.TwoTwo;
public ECrossJunctionMode CrossJunctionMode { get; init; } = ECrossJunctionMode.TwoTwoTwo;
public int JunctionMode { get; init; }
public ERemedyType RemedyType { get; init; }
public bool Depleted { get; init; }
public int ReactorId { get; init; }

View File

@@ -27,8 +27,7 @@ public sealed class SimulationEngine
var next = prop.Type switch {
EPropType.Flow or EPropType.Consumer => ToggleProp(level, position, prop),
EPropType.TJunction => level.SetProp(position, prop with { TJunctionMode = NextTJunctionMode(prop.TJunctionMode) }),
EPropType.CrossJunction => level.SetProp(position, prop with { CrossJunctionMode = NextCrossJunctionMode(prop.CrossJunctionMode) }),
EPropType.Junction => CycleJunctionMode(level, position, prop),
EPropType.Door => ToggleDoor(level, position),
EPropType.AllSeeingEyeTerminal => level with { Global = level.Global with { AllSeeingEyeUnlocked = true, Status = "ALL-SEEING-EYE ONLINE" } },
EPropType.RemedySupply => PickUpRemedy(level, position, prop),
@@ -631,14 +630,15 @@ public sealed class SimulationEngine
return level with { Global = level.Global with { Status = message } };
}
private static ETJunctionMode NextTJunctionMode(ETJunctionMode mode)
private static LevelState CycleJunctionMode(LevelState level, GridPosition position, PropState prop)
{
return mode == ETJunctionMode.FourZero ? ETJunctionMode.ZeroFour : mode + 1;
}
var flow = JunctionFlowAnalyzer.Analyze(level).FirstOrDefault(junction => junction.Position == position);
var outflowCount = flow?.OutgoingBranches.Count ?? 2;
var ratios = Balancing.Current.JunctionRatios(outflowCount);
if (ratios.Count == 0)
return level;
private static ECrossJunctionMode NextCrossJunctionMode(ECrossJunctionMode mode)
{
return mode == ECrossJunctionMode.TwoTwoTwo ? ECrossJunctionMode.ZeroThreeThree : mode + 1;
return level.SetProp(position, prop with { JunctionMode = (prop.JunctionMode + 1) % ratios.Count });
}
private static EBand SurfaceBand(SurfaceState surface, ECarrierType carrier)

View File

@@ -613,7 +613,7 @@ public sealed partial class MainWindow
return prop.Type switch {
EPropType.Flow => CarrierColor(prop.Carrier),
EPropType.Consumer => ColorHelper.FromArgb(255, 93, 123, 170),
EPropType.TJunction or EPropType.CrossJunction => ColorHelper.FromArgb(255, 143, 111, 178),
EPropType.Junction => ColorHelper.FromArgb(255, 143, 111, 178),
EPropType.Door => ColorHelper.FromArgb(255, 187, 119, 55),
EPropType.AllSeeingEyeTerminal => ColorHelper.FromArgb(255, 85, 151, 156),
EPropType.RemedySupply => ColorHelper.FromArgb(255, 76, 145, 86),
@@ -637,8 +637,7 @@ public sealed partial class MainWindow
return prop.Type switch {
EPropType.Flow => $"{CarrierShort(prop.Carrier)} SRC",
EPropType.Consumer => $"{CarrierShort(prop.Carrier)} CON",
EPropType.TJunction => $"T {prop.TJunctionMode}",
EPropType.CrossJunction => $"X {prop.CrossJunctionMode}",
EPropType.Junction => $"J {prop.JunctionMode}",
EPropType.Door => "DOOR",
EPropType.AllSeeingEyeTerminal => "EYE",
EPropType.RemedySupply => RemedyShort(prop.RemedyType),

View File

@@ -87,9 +87,9 @@ public sealed class SimulationEngineTests
}
[Fact]
public void TJunctionRatioSplitsFlowAcrossInferredOutgoingBranches()
public void JunctionRatioSplitsFlowAcrossInferredOutgoingBranches()
{
var level = BuildTJunctionLevel(ETJunctionMode.TwoTwo);
var level = BuildJunctionLevel(2);
var next = m_Engine.AdvanceTurn(level);
@@ -99,9 +99,9 @@ public sealed class SimulationEngineTests
}
[Fact]
public void TJunctionZeroWeightBranchReceivesNoIntentionalOutflow()
public void JunctionZeroWeightBranchReceivesNoIntentionalOutflow()
{
var level = BuildTJunctionLevel(ETJunctionMode.ZeroFour);
var level = BuildJunctionLevel(0);
var next = m_Engine.AdvanceTurn(level);
@@ -112,7 +112,7 @@ public sealed class SimulationEngineTests
[Fact]
public void ValidatorRejectsAmbiguousJunctionSourceBranches()
{
var level = BuildTJunctionLevel(ETJunctionMode.TwoTwo);
var level = BuildJunctionLevel(2);
level = level.SetProp(new(3, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
var report = new LevelValidator().Validate(level);
@@ -385,15 +385,15 @@ public sealed class SimulationEngineTests
return level;
}
private static LevelState BuildTJunctionLevel(ETJunctionMode mode)
private static LevelState BuildJunctionLevel(int mode)
{
var level = LevelState.Create("T junction", 6, 6);
var level = LevelState.Create("Junction", 6, 6);
level = level.SetUnderground(new(1, 3), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
level = level.SetUnderground(new(2, 3), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
level = level.SetUnderground(new(3, 3), ECarrierType.Fuel, new() { State = EUndergroundState.Intact });
level = level.SetProp(new(1, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
return level.SetProp(new(2, 3), new() { Type = EPropType.TJunction, Carrier = ECarrierType.Fuel, TJunctionMode = mode });
return level.SetProp(new(2, 3), new() { Type = EPropType.Junction, Carrier = ECarrierType.Fuel, JunctionMode = mode });
}
private static LevelState AddHorizontalUnderground(LevelState level, ECarrierType carrier, int startX, int endX, int y)