Compare commits
4 Commits
79f3219a72
...
b232c0319f
| Author | SHA1 | Date | |
|---|---|---|---|
| b232c0319f | |||
| 30963a9bde | |||
| 851f6d27e8 | |||
| ca41e009bd |
12
README.md
12
README.md
@@ -1,18 +1,20 @@
|
||||
# Reactor Maintenance
|
||||
# Reactor Maintenance
|
||||
|
||||
C# WinUI 3 + Win2D level editor for the deterministic grid simulation described in `design.md`.
|
||||
C# WinUI 3 + Win2D level editor for the deterministic grid simulation described in `docs/design.md`.
|
||||
|
||||
## Projects
|
||||
|
||||
- `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, forecasts, simulation turns, versioned JSON serialization, and swappable difficulty balancing profiles.
|
||||
- `src/ReactorMaintenance.Win2D`: Win2D editor app for painting floor/wall terrain plus cell props, loading/saving levels, advancing simulation turns, and activating the reactor.
|
||||
- `src/ReactorMaintenance.Simulation`: UI-independent level model, editor operations, validation, forecasts, simulation turns, versioned JSON serialization, and deterministic balancing defaults.
|
||||
- `src/ReactorMaintenance.Win2D`: Win2D editor app for authoring terrain, underground fuel/coolant/electricity networks, props, leaks, doors, surface hazards, robot start, loading/saving levels, ending turns, interacting with props, and activating a ready reactor.
|
||||
- `tests/ReactorMaintenance.Simulation.Tests`: unit tests for deterministic simulation behavior.
|
||||
|
||||
## Commands
|
||||
|
||||
```powershell
|
||||
dotnet test tests\ReactorMaintenance.Simulation.Tests\ReactorMaintenance.Simulation.Tests.csproj
|
||||
dotnet build src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64
|
||||
dotnet build src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64 -p:EnableWindowsTargeting=true
|
||||
dotnet run --project src\ReactorMaintenance.Win2D\ReactorMaintenance.Win2D.csproj -p:Platform=x64
|
||||
```
|
||||
|
||||
The WinUI/XAML compiler is Windows-specific. On Linux, the simulation tests run normally, but the Win2D app build must be verified in a Windows-capable environment.
|
||||
|
||||
|
||||
49
TASKS.md
Normal file
49
TASKS.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Reactor Maintenance Rewrite Tasks
|
||||
|
||||
## Current State
|
||||
|
||||
- Branch: `design-rewrite`
|
||||
- Scope approved: implement `docs/design.md` end-to-end with deterministic defaults and no backward compatibility.
|
||||
- Simulation core has been replaced with the first design-native model and deterministic engine slice.
|
||||
- Simulation and test projects now target `net10.0` because this Linux environment only has the .NET 10 runtime.
|
||||
- Win2D editor has been rewritten against the new design model.
|
||||
- Win2D project now targets `net10.0-windows10.0.19041.0` to match the simulation project.
|
||||
- Linux can restore and compile the referenced simulation project, but full WinUI/XAML compilation still requires a Windows-capable XAML compiler environment.
|
||||
|
||||
## Completed Work
|
||||
|
||||
- Read project instructions, Linux instructions, code style, and `docs/design.md`.
|
||||
- Confirmed deterministic balance defaults should be chosen during implementation.
|
||||
- Confirmed a full Win2D editor is required.
|
||||
- Created branch `design-rewrite`.
|
||||
- Added `TASKS.md` as the required per-commit work tracker.
|
||||
- Removed the legacy integer hazard/effect/hazard plug-in simulation surface.
|
||||
- Added design-native terrain, underground carrier layers, surface hazards, props, leaks, doors, reactor bindings, robot inventory, rule events, validation, serialization, and forecasts.
|
||||
- Added deterministic default balancing values.
|
||||
- Added a first deterministic simulation pipeline for network propagation, consumers, leaks, surface interactions, robot safety, reactor readiness, rule events, and forecasts.
|
||||
- Replaced old tests with design-based simulation tests.
|
||||
- Verified `dotnet test tests/ReactorMaintenance.Simulation.Tests/ReactorMaintenance.Simulation.Tests.csproj` passes: 11 passed.
|
||||
- Attempted `dotnet jb cleanupcode --build=False ...`; unavailable in this environment because `dotnet-jb` is not installed.
|
||||
- Reviewed the first slice and fixed an action-resolution maintainability issue before commit.
|
||||
- Verified `git diff --check` reports no whitespace errors.
|
||||
- Ran `dotnet jb cleanupcode --build=False ...` successfully after ReSharper install and normalized line endings back to LF.
|
||||
- Reworked the Win2D editor for the new model: full tool list, layer-aware painting, terrain, underground carriers, surface hazards, props, doors, leaks, robot, forecasts, save validation, starter level, and simple play actions.
|
||||
- Removed old editor dependencies on legacy props, pressure pipes, smoke, fire, and global power/cooling/core-stability fields.
|
||||
- Verified `dotnet test tests/ReactorMaintenance.Simulation.Tests/ReactorMaintenance.Simulation.Tests.csproj` passes after the editor rewrite: 11 passed.
|
||||
- Attempted Win2D build on Linux with `dotnet build src/ReactorMaintenance.Win2D/ReactorMaintenance.Win2D.csproj -p:EnableWindowsTargeting=true -p:Platform=x64`; it fails at Windows `XamlCompiler.exe` with exec format error.
|
||||
- Attempted managed XAML compiler path with `-p:UseXamlCompilerExecutable=false`; it fails loading the WinUI XAML compiler task dependency under this Linux/.NET 10 setup.
|
||||
- Updated `README.md` for the new design-model editor, .NET 10 target, and Linux/Windows build expectations.
|
||||
|
||||
## Current Work
|
||||
|
||||
- Commit the Win2D editor rewrite slice.
|
||||
|
||||
## Future Work
|
||||
|
||||
1. Expand simulation fidelity where the first slice is intentionally simplified: junction branch inference, ambiguity validation, complete pair table coverage, richer rule predicates/effects, and stronger forecast proof cases.
|
||||
2. Add advanced editor workflows for explicit reactor binding selection, explicit door edge selection, electricity wall leak face selection, and rule event authoring.
|
||||
3. Verify and polish the Win2D app on Windows where the XAML compiler can run.
|
||||
4. Update README and any affected docs to reflect the new schema, .NET target, editor controls, and deterministic defaults.
|
||||
5. Build the Win2D project on a Windows-capable environment after the editor rewrite.
|
||||
6. Add broader tests for junction ratios, ambiguous junctions, all rule event families, serialization edge cases, and editor operations.
|
||||
7. Run cleanup when `dotnet-jb` is available, tests, code review, and iterate until the implementation is clean and maintainable.
|
||||
13
dotnet-tools.json
Normal file
13
dotnet-tools.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"jetbrains.resharper.globaltools": {
|
||||
"version": "2026.1.1",
|
||||
"commands": [
|
||||
"jb"
|
||||
],
|
||||
"rollForward": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using ReactorMaintenance.Simulation.Difficulties;
|
||||
using ReactorMaintenance.Simulation.Difficulties;
|
||||
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
@@ -6,73 +6,67 @@ public abstract class Balancing
|
||||
{
|
||||
public static Balancing Current { get; set; } = new NormalBalancing();
|
||||
|
||||
public abstract int MinHazardValue { get; }
|
||||
public abstract int MaxHazardValue { get; }
|
||||
public abstract int DefaultHazardStability { get; }
|
||||
public abstract int DefaultCellIntegrity { get; }
|
||||
public abstract int DefaultActionsPerTurn { get; }
|
||||
public abstract int DefaultCoreHeat { get; }
|
||||
public abstract int DefaultFacilityStability { get; }
|
||||
public abstract int DefaultPower { get; }
|
||||
public abstract int DefaultCooling { get; }
|
||||
public abstract int FirstGridCoordinate { get; }
|
||||
public abstract int NeighborDistance { get; }
|
||||
public abstract int CurrentForecastTurn { get; }
|
||||
public abstract int MinimumLevelSize { get; }
|
||||
public float ClampValue(float value)
|
||||
{
|
||||
return Math.Clamp(value, MinValue, MaxValue);
|
||||
}
|
||||
|
||||
public EBand Band(float value, float caution, float critical)
|
||||
{
|
||||
if (value >= critical)
|
||||
return EBand.Critical;
|
||||
|
||||
return value >= caution ? EBand.Caution : EBand.Safe;
|
||||
}
|
||||
|
||||
public abstract int DefaultLevelWidth { get; }
|
||||
public abstract int DefaultLevelHeight { get; }
|
||||
public abstract int DefaultRobotCoordinate { get; }
|
||||
public abstract int DefaultPipeFlow { get; }
|
||||
public abstract int DefaultPipePressure { get; }
|
||||
public abstract int DefaultPressurePipeFlow { get; }
|
||||
public abstract int DefaultPressurePipePressure { get; }
|
||||
public abstract int DefaultEditedPipeIntegrity { get; }
|
||||
public abstract int MinimumLeakRate { get; }
|
||||
public abstract int DamagedPipeIntegrity { get; }
|
||||
public abstract int RepairedLeakRate { get; }
|
||||
public abstract int RepairedElectricalCharge { get; }
|
||||
public abstract int HeatToolIncrease { get; }
|
||||
public abstract int FireToolMinimumHeat { get; }
|
||||
public abstract int FireToolMinimumSmoke { get; }
|
||||
public abstract int MaxForecastStepCount { get; }
|
||||
public abstract int TurnIncrement { get; }
|
||||
public abstract int OverpressureThreshold { get; }
|
||||
public abstract int HeatIntegrityDamageThreshold { get; }
|
||||
public abstract int PipeFireIntegrityDamage { get; }
|
||||
public abstract int FireStabilityDamage { get; }
|
||||
public abstract int BurstLeakRate { get; }
|
||||
public abstract int BrokenPipeFlow { get; }
|
||||
public abstract int ElectrifiedCoolantPoolingThreshold { get; }
|
||||
public abstract int ElectricalChargeIncrease { get; }
|
||||
public abstract int FuelVaporFireThreshold { get; }
|
||||
public abstract int LiquidFuelFireThreshold { get; }
|
||||
public abstract int HeatIgnitionThreshold { get; }
|
||||
public abstract int ElectricalIgnitionThreshold { get; }
|
||||
public abstract int FireHeatIncrease { get; }
|
||||
public abstract int FireSmokeIncrease { get; }
|
||||
public abstract int FireLiquidFuelConsumption { get; }
|
||||
public abstract int FireFuelVaporConsumption { get; }
|
||||
public abstract int SmokeDecay { get; }
|
||||
public abstract int PressurizedFuelLeakPressureThreshold { get; }
|
||||
public abstract int PassiveFuelVaporHeatOffset { get; }
|
||||
public abstract int PassiveFuelVaporDivisor { get; }
|
||||
public abstract int MinimumCoolantHeatReduction { get; }
|
||||
public abstract int CoolantHeatReductionDivisor { get; }
|
||||
public abstract int CoolantSteamHeatThreshold { get; }
|
||||
public abstract int CoolantSteamSmokeIncrease { get; }
|
||||
public abstract int PressureLeakSmokeThreshold { get; }
|
||||
public abstract int PressureLeakSmokeIncrease { get; }
|
||||
public abstract int GeneratorHeatIncrease { get; }
|
||||
public abstract int CoolingPumpHeatReduction { get; }
|
||||
public abstract int ReactorHeatIncrease { get; }
|
||||
public abstract int SmokeSpreadThreshold { get; }
|
||||
public abstract int SmokeSpreadIncrease { get; }
|
||||
public abstract int CriticalCellStabilityThreshold { get; }
|
||||
public abstract int MeltdownCoreHeatThreshold { get; }
|
||||
public abstract int StabilityCollapseThreshold { get; }
|
||||
public abstract int GeneratorPowerOutput { get; }
|
||||
public abstract int CoolingPumpOutput { get; }
|
||||
public abstract int ReactorReadyPowerThreshold { get; }
|
||||
public abstract int ReactorReadyCoolingThreshold { get; }
|
||||
public abstract int ReactorReadyCoreHeatThreshold { get; }
|
||||
public abstract int MinimumLevelSize { get; }
|
||||
public abstract int ActionsPerTurn { get; }
|
||||
public abstract int ForecastHorizon { get; }
|
||||
public abstract float MinValue { get; }
|
||||
public abstract float MaxValue { get; }
|
||||
public abstract float FuelSafe { get; }
|
||||
public abstract float FuelCaution { get; }
|
||||
public abstract float FuelCritical { get; }
|
||||
public abstract float CoolantSafe { get; }
|
||||
public abstract float CoolantCaution { get; }
|
||||
public abstract float CoolantCritical { get; }
|
||||
public abstract float ElectricitySafe { get; }
|
||||
public abstract float ElectricityCaution { get; }
|
||||
public abstract float ElectricityCritical { get; }
|
||||
public abstract float HeatSafe { get; }
|
||||
public abstract float HeatCaution { get; }
|
||||
public abstract float HeatCritical { get; }
|
||||
public abstract float TerminalHeat { get; }
|
||||
public abstract float RobotFuelSafetyThreshold { get; }
|
||||
public abstract float RobotCoolantSafetyThreshold { get; }
|
||||
public abstract float RobotElectricitySafetyThreshold { get; }
|
||||
public abstract float RobotHeatSafetyThreshold { get; }
|
||||
public abstract float SourceAmount { get; }
|
||||
public abstract float SourceIntensity { get; }
|
||||
public abstract float DistanceAmountFalloff { get; }
|
||||
public abstract float DistanceIntensityFalloff { get; }
|
||||
public abstract float ConsumerRequiredAmount { get; }
|
||||
public abstract float ConsumerRequiredIntensity { get; }
|
||||
public abstract float LeakBaseAmount { get; }
|
||||
public abstract float LeakAmountScale { get; }
|
||||
public abstract float LeakIntensityScale { get; }
|
||||
public abstract float FlowTransferRatio { get; }
|
||||
public abstract float StrongFlowTransferRatio { get; }
|
||||
public abstract float Warm1Amount { get; }
|
||||
public abstract float Warm2Amount { get; }
|
||||
public abstract float Quench1Amount { get; }
|
||||
public abstract float Quench2Amount { get; }
|
||||
public abstract float Short1Heat { get; }
|
||||
public abstract float Short1Discharge { get; }
|
||||
public abstract float Short2Heat { get; }
|
||||
public abstract float Short2Discharge { get; }
|
||||
public abstract float Ignite1Heat { get; }
|
||||
public abstract float Ignite1FuelConsumption { get; }
|
||||
public abstract float Ignite2Heat { get; }
|
||||
public abstract float Ignite2FuelConsumption { get; }
|
||||
public abstract int RemedyBlockTurns { get; }
|
||||
public abstract int HeatShieldSteps { get; }
|
||||
public abstract int InventoryCapacityPerRemedy { get; }
|
||||
}
|
||||
@@ -1,76 +1,55 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Difficulties;
|
||||
|
||||
public class NormalBalancing : Balancing
|
||||
{
|
||||
public override int MinHazardValue => 0;
|
||||
public override int MaxHazardValue => 10;
|
||||
public override int DefaultHazardStability => 10;
|
||||
public override int DefaultCellIntegrity => 10;
|
||||
public override int DefaultActionsPerTurn => 3;
|
||||
public override int DefaultCoreHeat => 5;
|
||||
public override int DefaultFacilityStability => 10;
|
||||
public override int DefaultPower => 5;
|
||||
public override int DefaultCooling => 0;
|
||||
public override int FirstGridCoordinate => 0;
|
||||
public override int NeighborDistance => 1;
|
||||
public override int CurrentForecastTurn => 0;
|
||||
public override int MinimumLevelSize => 4;
|
||||
public override int DefaultLevelWidth => 16;
|
||||
public override int DefaultLevelHeight => 12;
|
||||
public override int DefaultRobotCoordinate => 1;
|
||||
public override int DefaultPipeFlow => 4;
|
||||
public override int DefaultPipePressure => 4;
|
||||
public override int DefaultPressurePipeFlow => 5;
|
||||
public override int DefaultPressurePipePressure => 6;
|
||||
public override int DefaultEditedPipeIntegrity => 8;
|
||||
public override int MinimumLeakRate => 1;
|
||||
public override int DamagedPipeIntegrity => 4;
|
||||
public override int RepairedLeakRate => 0;
|
||||
public override int RepairedElectricalCharge => 0;
|
||||
public override int HeatToolIncrease => 2;
|
||||
public override int FireToolMinimumHeat => 7;
|
||||
public override int FireToolMinimumSmoke => 3;
|
||||
public override int MaxForecastStepCount => 12;
|
||||
public override int TurnIncrement => 1;
|
||||
public override int OverpressureThreshold => 7;
|
||||
public override int HeatIntegrityDamageThreshold => 10;
|
||||
public override int PipeFireIntegrityDamage => 1;
|
||||
public override int FireStabilityDamage => 1;
|
||||
public override int BurstLeakRate => 3;
|
||||
public override int BrokenPipeFlow => 0;
|
||||
public override int ElectrifiedCoolantPoolingThreshold => 3;
|
||||
public override int ElectricalChargeIncrease => 2;
|
||||
public override int FuelVaporFireThreshold => 4;
|
||||
public override int LiquidFuelFireThreshold => 6;
|
||||
public override int HeatIgnitionThreshold => 8;
|
||||
public override int ElectricalIgnitionThreshold => 4;
|
||||
public override int FireHeatIncrease => 2;
|
||||
public override int FireSmokeIncrease => 2;
|
||||
public override int FireLiquidFuelConsumption => 1;
|
||||
public override int FireFuelVaporConsumption => 1;
|
||||
public override int SmokeDecay => 1;
|
||||
public override int PressurizedFuelLeakPressureThreshold => 7;
|
||||
public override int PassiveFuelVaporHeatOffset => 3;
|
||||
public override int PassiveFuelVaporDivisor => 3;
|
||||
public override int MinimumCoolantHeatReduction => 1;
|
||||
public override int CoolantHeatReductionDivisor => 2;
|
||||
public override int CoolantSteamHeatThreshold => 7;
|
||||
public override int CoolantSteamSmokeIncrease => 2;
|
||||
public override int PressureLeakSmokeThreshold => 8;
|
||||
public override int PressureLeakSmokeIncrease => 1;
|
||||
public override int GeneratorHeatIncrease => 1;
|
||||
public override int CoolingPumpHeatReduction => 2;
|
||||
public override int ReactorHeatIncrease => 1;
|
||||
public override int SmokeSpreadThreshold => 6;
|
||||
public override int SmokeSpreadIncrease => 1;
|
||||
public override int CriticalCellStabilityThreshold => 3;
|
||||
public override int MeltdownCoreHeatThreshold => 10;
|
||||
public override int StabilityCollapseThreshold => 0;
|
||||
public override int GeneratorPowerOutput => 3;
|
||||
public override int CoolingPumpOutput => 3;
|
||||
public override int ReactorReadyPowerThreshold => 3;
|
||||
public override int ReactorReadyCoolingThreshold => 3;
|
||||
public override int ReactorReadyCoreHeatThreshold => 8;
|
||||
public override int MinimumLevelSize => 4;
|
||||
public override int ActionsPerTurn => 3;
|
||||
public override int ForecastHorizon => 6;
|
||||
public override float MinValue => 0;
|
||||
public override float MaxValue => 10;
|
||||
public override float FuelSafe => 1.5f;
|
||||
public override float FuelCaution => 3.5f;
|
||||
public override float FuelCritical => 6.5f;
|
||||
public override float CoolantSafe => 1.5f;
|
||||
public override float CoolantCaution => 3.5f;
|
||||
public override float CoolantCritical => 6.5f;
|
||||
public override float ElectricitySafe => 1.5f;
|
||||
public override float ElectricityCaution => 3.5f;
|
||||
public override float ElectricityCritical => 6.5f;
|
||||
public override float HeatSafe => 2;
|
||||
public override float HeatCaution => 5;
|
||||
public override float HeatCritical => 8;
|
||||
public override float TerminalHeat => 10;
|
||||
public override float RobotFuelSafetyThreshold => 6.5f;
|
||||
public override float RobotCoolantSafetyThreshold => 8;
|
||||
public override float RobotElectricitySafetyThreshold => 6.5f;
|
||||
public override float RobotHeatSafetyThreshold => 8;
|
||||
public override float SourceAmount => 8;
|
||||
public override float SourceIntensity => 8;
|
||||
public override float DistanceAmountFalloff => 0.5f;
|
||||
public override float DistanceIntensityFalloff => 0.4f;
|
||||
public override float ConsumerRequiredAmount => 2.5f;
|
||||
public override float ConsumerRequiredIntensity => 2.5f;
|
||||
public override float LeakBaseAmount => 0.5f;
|
||||
public override float LeakAmountScale => 0.15f;
|
||||
public override float LeakIntensityScale => 0.1f;
|
||||
public override float FlowTransferRatio => 0.05f;
|
||||
public override float StrongFlowTransferRatio => 0.1f;
|
||||
public override float Warm1Amount => 0.5f;
|
||||
public override float Warm2Amount => 1.0f;
|
||||
public override float Quench1Amount => 0.6f;
|
||||
public override float Quench2Amount => 1.2f;
|
||||
public override float Short1Heat => 0.8f;
|
||||
public override float Short1Discharge => 0.8f;
|
||||
public override float Short2Heat => 1.6f;
|
||||
public override float Short2Discharge => 1.5f;
|
||||
public override float Ignite1Heat => 1.2f;
|
||||
public override float Ignite1FuelConsumption => 0.4f;
|
||||
public override float Ignite2Heat => 2.4f;
|
||||
public override float Ignite2FuelConsumption => 0.8f;
|
||||
public override int RemedyBlockTurns => 2;
|
||||
public override int HeatShieldSteps => 3;
|
||||
public override int InventoryCapacityPerRemedy => 3;
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Effects;
|
||||
|
||||
public sealed class CellIntegrityEffect : ISimulationEffect
|
||||
{
|
||||
public CellState Apply(CellState cell)
|
||||
{
|
||||
var integrity = cell.Integrity;
|
||||
var hazards = cell.Hazards;
|
||||
|
||||
if (cell is { HasPipe: true } && cell.Pressure > Balancing.Current.OverpressureThreshold)
|
||||
integrity -= cell.Pressure - Balancing.Current.OverpressureThreshold;
|
||||
|
||||
if (hazards.Heat >= Balancing.Current.HeatIntegrityDamageThreshold || hazards.Fire)
|
||||
{
|
||||
integrity -= cell.HasPipe ? Balancing.Current.PipeFireIntegrityDamage : Balancing.Current.MinHazardValue;
|
||||
hazards = hazards with { Stability = hazards.Stability - Balancing.Current.FireStabilityDamage };
|
||||
}
|
||||
|
||||
cell = cell with {
|
||||
Integrity = Rules.Clamp(integrity),
|
||||
Hazards = hazards.Clamp()
|
||||
};
|
||||
|
||||
if (integrity > Balancing.Current.MinHazardValue || !cell.HasPipe)
|
||||
return cell;
|
||||
|
||||
return cell with {
|
||||
LeakRate = Math.Max(cell.LeakRate, Balancing.Current.BurstLeakRate),
|
||||
Flow = Balancing.Current.BrokenPipeFlow,
|
||||
PipeOpen = false
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Effects;
|
||||
|
||||
public sealed class FireAndElectricalHazardEffect : ISimulationEffect
|
||||
{
|
||||
public CellState Apply(CellState cell)
|
||||
{
|
||||
var hazards = cell.Hazards;
|
||||
if (hazards.CoolantPooling >= Balancing.Current.ElectrifiedCoolantPoolingThreshold && cell.Powered)
|
||||
hazards = hazards with { ElectricalCharge = hazards.ElectricalCharge + Balancing.Current.ElectricalChargeIncrease };
|
||||
|
||||
var hasFuel = hazards.FuelVapor >= Balancing.Current.FuelVaporFireThreshold || hazards.LiquidFuel >= Balancing.Current.LiquidFuelFireThreshold;
|
||||
var hasIgnition = hazards.Heat >= Balancing.Current.HeatIgnitionThreshold || hazards.ElectricalCharge >= Balancing.Current.ElectricalIgnitionThreshold || cell is { Prop: ECellProp.Generator, Powered: true };
|
||||
if ((hasFuel && hasIgnition) || hazards.Fire)
|
||||
{
|
||||
hazards = hazards with {
|
||||
Fire = hasFuel || hazards.Fire,
|
||||
Heat = hazards.Heat + Balancing.Current.FireHeatIncrease,
|
||||
Smoke = hazards.Smoke + Balancing.Current.FireSmokeIncrease,
|
||||
LiquidFuel = Math.Max(Balancing.Current.MinHazardValue, hazards.LiquidFuel - Balancing.Current.FireLiquidFuelConsumption),
|
||||
FuelVapor = Math.Max(Balancing.Current.MinHazardValue, hazards.FuelVapor - Balancing.Current.FireFuelVaporConsumption)
|
||||
};
|
||||
}
|
||||
else if (hazards.Smoke > Balancing.Current.MinHazardValue)
|
||||
hazards = hazards with { Smoke = hazards.Smoke - Balancing.Current.SmokeDecay };
|
||||
|
||||
return cell with { Hazards = hazards.Clamp() };
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Effects;
|
||||
|
||||
public interface IAreaSimulationEffect
|
||||
{
|
||||
CellState[] Apply(LevelState level, CellState[] cells);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Effects;
|
||||
|
||||
public interface ISimulationEffect
|
||||
{
|
||||
CellState Apply(CellState cell);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Effects;
|
||||
|
||||
public sealed class MachineEffect : ISimulationEffect
|
||||
{
|
||||
public CellState Apply(CellState cell)
|
||||
{
|
||||
var hazards = cell.Prop switch {
|
||||
ECellProp.Generator when cell.Powered => cell.Hazards with { Heat = cell.Hazards.Heat + Balancing.Current.GeneratorHeatIncrease },
|
||||
ECellProp.CoolingPump when cell.Powered => cell.Hazards with { Heat = cell.Hazards.Heat - Balancing.Current.CoolingPumpHeatReduction },
|
||||
ECellProp.Reactor => cell.Hazards with { Heat = cell.Hazards.Heat + Balancing.Current.ReactorHeatIncrease },
|
||||
_ => cell.Hazards
|
||||
};
|
||||
|
||||
return cell with { Hazards = hazards.Clamp() };
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Effects;
|
||||
|
||||
public sealed class PipeLeakEffect : ISimulationEffect
|
||||
{
|
||||
public CellState Apply(CellState cell)
|
||||
{
|
||||
if (!cell.HasPipe || cell.LeakRate <= Balancing.Current.MinHazardValue)
|
||||
return cell;
|
||||
|
||||
var hazards = cell.Pipe switch {
|
||||
EPipeMedium.Fuel => cell.Hazards with {
|
||||
LiquidFuel = cell.Hazards.LiquidFuel + cell.LeakRate,
|
||||
FuelVapor = cell.Hazards.FuelVapor + (cell.Pressure >= Balancing.Current.PressurizedFuelLeakPressureThreshold ? cell.LeakRate : Math.Max(Balancing.Current.MinHazardValue, cell.Hazards.Heat - Balancing.Current.PassiveFuelVaporHeatOffset) / Balancing.Current.PassiveFuelVaporDivisor)
|
||||
},
|
||||
EPipeMedium.Coolant => cell.Hazards with {
|
||||
CoolantPooling = cell.Hazards.CoolantPooling + cell.LeakRate,
|
||||
Heat = cell.Hazards.Heat - Math.Max(Balancing.Current.MinimumCoolantHeatReduction, cell.LeakRate / Balancing.Current.CoolantHeatReductionDivisor),
|
||||
Smoke = cell.Hazards.Smoke + (cell.Hazards.Heat >= Balancing.Current.CoolantSteamHeatThreshold ? Balancing.Current.CoolantSteamSmokeIncrease : Balancing.Current.MinHazardValue)
|
||||
},
|
||||
EPipeMedium.Pressure => cell.Hazards with { Smoke = cell.Hazards.Smoke + (cell.Pressure >= Balancing.Current.PressureLeakSmokeThreshold ? Balancing.Current.PressureLeakSmokeIncrease : Balancing.Current.MinHazardValue) },
|
||||
_ => cell.Hazards
|
||||
};
|
||||
|
||||
return cell with { Hazards = hazards.Clamp() };
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Effects;
|
||||
|
||||
public sealed class SmokeSpreadEffect : IAreaSimulationEffect
|
||||
{
|
||||
public CellState[] Apply(LevelState level, CellState[] cells)
|
||||
{
|
||||
var next = cells.ToArray();
|
||||
for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
|
||||
{
|
||||
for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
var cell = cells[level.Index(position)];
|
||||
if (cell.Hazards.Smoke < Balancing.Current.SmokeSpreadThreshold)
|
||||
continue;
|
||||
|
||||
SpreadToNeighbors(level, next, position);
|
||||
}
|
||||
}
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
private static void SpreadToNeighbors(LevelState level, CellState[] next, GridPosition position)
|
||||
{
|
||||
foreach (var neighbor in position.Neighbors().Where(level.InBounds))
|
||||
{
|
||||
var neighborCell = next[level.Index(neighbor)];
|
||||
if (!neighborCell.IsWalkable || neighborCell.DoorLocked)
|
||||
continue;
|
||||
|
||||
next[level.Index(neighbor)] = neighborCell with { Hazards = neighborCell.Hazards with { Smoke = Rules.Clamp(neighborCell.Hazards.Smoke + Balancing.Current.SmokeSpreadIncrease) } };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Hazards;
|
||||
|
||||
public abstract class Hazard
|
||||
{
|
||||
public abstract IEnumerable<Forecast> Predict(LevelState level, int turns);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Hazards;
|
||||
|
||||
public sealed class IgnitionHazard : Hazard
|
||||
{
|
||||
public override IEnumerable<Forecast> Predict(LevelState level, int turns)
|
||||
{
|
||||
for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
|
||||
{
|
||||
for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
var cell = level.GetCell(position);
|
||||
if (cell.Hazards.Fire)
|
||||
yield return new(EFailureKind.Ignition, position, turns, turns == Balancing.Current.TurnIncrement ? $"FUEL IGNITION PREDICTED AT {x},{y} NEXT TURN" : $"FUEL IGNITION PREDICTED AT {x},{y} IN {turns} TURNS");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Hazards;
|
||||
|
||||
public sealed class MeltdownHazard : Hazard
|
||||
{
|
||||
public override IEnumerable<Forecast> Predict(LevelState level, int turns)
|
||||
{
|
||||
if (level.Global is { Lost: true, Status: "CORE MELTDOWN" })
|
||||
yield return new(EFailureKind.Meltdown, null, turns, "CORE MELTDOWN APPROACHING");
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Hazards;
|
||||
|
||||
public sealed class PipeBurstHazard : Hazard
|
||||
{
|
||||
public override IEnumerable<Forecast> Predict(LevelState level, int turns)
|
||||
{
|
||||
for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
|
||||
{
|
||||
for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
var cell = level.GetCell(position);
|
||||
if (cell is { HasPipe: true, PipeOpen: false } && cell.Flow == Balancing.Current.BrokenPipeFlow && cell.LeakRate >= Balancing.Current.BurstLeakRate)
|
||||
yield return new(EFailureKind.PipeBurst, position, turns, $"PIPE BURST PREDICTED AT {x},{y} IN {turns} TURNS");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
using ReactorMaintenance.Simulation;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Hazards;
|
||||
|
||||
public sealed class StabilityCollapseHazard : Hazard
|
||||
{
|
||||
public override IEnumerable<Forecast> Predict(LevelState level, int turns)
|
||||
{
|
||||
if (level.Global is { Lost: true, Status: "FACILITY STABILITY COLLAPSE" })
|
||||
yield return new(EFailureKind.StabilityCollapse, null, turns, "FACILITY STABILITY COLLAPSE APPROACHING");
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,35 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum EEditorTool
|
||||
{
|
||||
Cursor,
|
||||
Floor,
|
||||
Wall,
|
||||
Reactor,
|
||||
CoolingPump,
|
||||
Generator,
|
||||
PressureRegulator,
|
||||
DiagnosticTerminal,
|
||||
ControlTerminal,
|
||||
CoolantPipe,
|
||||
FuelPipe,
|
||||
PressurePipe,
|
||||
Leak,
|
||||
Repair,
|
||||
FuelUnderground,
|
||||
CoolantUnderground,
|
||||
ElectricityUnderground,
|
||||
FuelFlow,
|
||||
CoolantFlow,
|
||||
ElectricityFlow,
|
||||
FuelConsumer,
|
||||
CoolantConsumer,
|
||||
ElectricityConsumer,
|
||||
TJunction,
|
||||
CrossJunction,
|
||||
Door,
|
||||
AllSeeingEyeTerminal,
|
||||
FuelRemedySupply,
|
||||
CoolantRemedySupply,
|
||||
ElectricityRemedySupply,
|
||||
HeatRemedySupply,
|
||||
ReactorControl,
|
||||
FuelLeak,
|
||||
CoolantLeak,
|
||||
ElectricityLeak,
|
||||
FuelHazard,
|
||||
CoolantHazard,
|
||||
ElectricityHazard,
|
||||
Heat,
|
||||
Fire,
|
||||
Robot
|
||||
}
|
||||
|
||||
@@ -28,92 +40,126 @@ public static class LevelEditor
|
||||
if (!level.InBounds(position))
|
||||
return level;
|
||||
|
||||
if (tool == EEditorTool.Robot)
|
||||
return level.GetCell(position).IsWalkable ? level with { Robot = position } : level;
|
||||
|
||||
var cell = level.GetCell(position);
|
||||
cell = tool switch {
|
||||
EEditorTool.Cursor => cell,
|
||||
EEditorTool.Floor => cell with { Terrain = ECellTerrain.Floor },
|
||||
EEditorTool.Wall => cell with {
|
||||
Terrain = ECellTerrain.Wall,
|
||||
Prop = ECellProp.None,
|
||||
Pipe = EPipeMedium.None,
|
||||
Flow = Balancing.Current.MinHazardValue,
|
||||
Pressure = Balancing.Current.MinHazardValue,
|
||||
LeakRate = Balancing.Current.MinHazardValue,
|
||||
PipeOpen = false,
|
||||
Powered = false
|
||||
},
|
||||
EEditorTool.Reactor => cell with { Terrain = ECellTerrain.Floor, Prop = ECellProp.Reactor },
|
||||
EEditorTool.CoolingPump => cell with {
|
||||
Terrain = ECellTerrain.Floor,
|
||||
Prop = ECellProp.CoolingPump,
|
||||
Powered = true
|
||||
},
|
||||
EEditorTool.Generator => cell with {
|
||||
Terrain = ECellTerrain.Floor,
|
||||
Prop = ECellProp.Generator,
|
||||
Powered = true
|
||||
},
|
||||
EEditorTool.PressureRegulator => cell with { Terrain = ECellTerrain.Floor, Prop = ECellProp.PressureRegulator },
|
||||
EEditorTool.DiagnosticTerminal => cell with {
|
||||
Terrain = ECellTerrain.Floor,
|
||||
Prop = ECellProp.DiagnosticTerminal,
|
||||
Powered = true
|
||||
},
|
||||
EEditorTool.ControlTerminal => cell with {
|
||||
Terrain = ECellTerrain.Floor,
|
||||
Prop = ECellProp.ControlTerminal,
|
||||
Powered = true
|
||||
},
|
||||
EEditorTool.CoolantPipe => cell with {
|
||||
Pipe = EPipeMedium.Coolant,
|
||||
Flow = Balancing.Current.DefaultPipeFlow,
|
||||
Pressure = Balancing.Current.DefaultPipePressure,
|
||||
Integrity = Math.Max(cell.Integrity, Balancing.Current.DefaultEditedPipeIntegrity),
|
||||
PipeOpen = true
|
||||
},
|
||||
EEditorTool.FuelPipe => cell with {
|
||||
Pipe = EPipeMedium.Fuel,
|
||||
Flow = Balancing.Current.DefaultPipeFlow,
|
||||
Pressure = Balancing.Current.DefaultPipePressure,
|
||||
Integrity = Math.Max(cell.Integrity, Balancing.Current.DefaultEditedPipeIntegrity),
|
||||
PipeOpen = true
|
||||
},
|
||||
EEditorTool.PressurePipe => cell with {
|
||||
Pipe = EPipeMedium.Pressure,
|
||||
Flow = Balancing.Current.DefaultPressurePipeFlow,
|
||||
Pressure = Balancing.Current.DefaultPressurePipePressure,
|
||||
Integrity = Math.Max(cell.Integrity, Balancing.Current.DefaultEditedPipeIntegrity),
|
||||
PipeOpen = true
|
||||
},
|
||||
EEditorTool.Leak => cell with {
|
||||
LeakRate = Math.Max(Balancing.Current.MinimumLeakRate, cell.LeakRate),
|
||||
Integrity = Math.Min(cell.Integrity, Balancing.Current.DamagedPipeIntegrity)
|
||||
},
|
||||
EEditorTool.Repair => cell with {
|
||||
LeakRate = Balancing.Current.RepairedLeakRate,
|
||||
Integrity = Balancing.Current.DefaultCellIntegrity,
|
||||
Hazards = cell.Hazards with {
|
||||
Fire = false,
|
||||
ElectricalCharge = Balancing.Current.RepairedElectricalCharge
|
||||
}
|
||||
},
|
||||
EEditorTool.Heat => cell with { Hazards = cell.Hazards with { Heat = Rules.Clamp(cell.Hazards.Heat + Balancing.Current.HeatToolIncrease) } },
|
||||
EEditorTool.Fire => cell with {
|
||||
Hazards = cell.Hazards with {
|
||||
Fire = !cell.Hazards.Fire,
|
||||
Heat = Math.Max(cell.Hazards.Heat, Balancing.Current.FireToolMinimumHeat),
|
||||
Smoke = Math.Max(cell.Hazards.Smoke, Balancing.Current.FireToolMinimumSmoke)
|
||||
}
|
||||
},
|
||||
_ => cell
|
||||
return tool switch {
|
||||
EEditorTool.Cursor => level,
|
||||
EEditorTool.Floor => level.SetTerrain(position, ECellTerrain.Floor),
|
||||
EEditorTool.Wall => level.SetTerrain(position, ECellTerrain.Wall),
|
||||
EEditorTool.FuelUnderground => SetUnderground(level, position, ECarrierType.Fuel),
|
||||
EEditorTool.CoolantUnderground => SetUnderground(level, position, ECarrierType.Coolant),
|
||||
EEditorTool.ElectricityUnderground => SetUnderground(level, position, ECarrierType.Electricity),
|
||||
EEditorTool.FuelFlow => SetCarrierProp(level, position, EPropType.Flow, ECarrierType.Fuel),
|
||||
EEditorTool.CoolantFlow => SetCarrierProp(level, position, EPropType.Flow, ECarrierType.Coolant),
|
||||
EEditorTool.ElectricityFlow => SetCarrierProp(level, position, EPropType.Flow, ECarrierType.Electricity),
|
||||
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.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 }),
|
||||
EEditorTool.CoolantRemedySupply => SetFloorProp(level, position, new() { Type = EPropType.RemedySupply, RemedyType = ERemedyType.CoolantNeutralizer }),
|
||||
EEditorTool.ElectricityRemedySupply => SetFloorProp(level, position, new() { Type = EPropType.RemedySupply, RemedyType = ERemedyType.ElectricityNeutralizer }),
|
||||
EEditorTool.HeatRemedySupply => SetFloorProp(level, position, new() { Type = EPropType.RemedySupply, RemedyType = ERemedyType.HeatShield }),
|
||||
EEditorTool.ReactorControl => SetReactorControl(level, position),
|
||||
EEditorTool.FuelLeak => SetLeak(level, position, ECarrierType.Fuel),
|
||||
EEditorTool.CoolantLeak => SetLeak(level, position, ECarrierType.Coolant),
|
||||
EEditorTool.ElectricityLeak => SetLeak(level, position, ECarrierType.Electricity),
|
||||
EEditorTool.FuelHazard => level.SetSurface(position, level.GetSurface(position) with { Fuel = level.GetSurface(position).Fuel + 1 }),
|
||||
EEditorTool.CoolantHazard => level.SetSurface(position, level.GetSurface(position) with { Coolant = level.GetSurface(position).Coolant + 1 }),
|
||||
EEditorTool.ElectricityHazard => level.SetSurface(position, level.GetSurface(position) with { Electricity = level.GetSurface(position).Electricity + 1 }),
|
||||
EEditorTool.Heat => level.SetSurface(position, level.GetSurface(position) with { Heat = level.GetSurface(position).Heat + 1 }),
|
||||
EEditorTool.Robot => level.IsFloor(position) ? level with { Robot = level.Robot with { Position = position } } : level,
|
||||
_ => level
|
||||
};
|
||||
}
|
||||
|
||||
if (cell.Terrain == ECellTerrain.Wall)
|
||||
cell = cell with { Hazards = new() };
|
||||
public static LevelState BindFirstReactorToConsumers(LevelState level, GridPosition fuelConsumer, GridPosition coolantConsumer, GridPosition electricityConsumer)
|
||||
{
|
||||
if (level.Reactors.Count == 0)
|
||||
return level;
|
||||
|
||||
return level.SetCell(position, cell);
|
||||
var reactors = level.Reactors.ToArray();
|
||||
reactors[0] = reactors[0] with {
|
||||
FuelConsumerPosition = fuelConsumer,
|
||||
CoolantConsumerPosition = coolantConsumer,
|
||||
ElectricityConsumerPosition = electricityConsumer
|
||||
};
|
||||
return level with { Reactors = reactors };
|
||||
}
|
||||
|
||||
private static LevelState SetUnderground(LevelState level, GridPosition position, ECarrierType carrier)
|
||||
{
|
||||
return level.SetUnderground(position, carrier, new() { State = EUndergroundState.Intact });
|
||||
}
|
||||
|
||||
private static LevelState SetCarrierProp(LevelState level, GridPosition position, EPropType type, ECarrierType carrier)
|
||||
{
|
||||
return SetFloorProp(level, position, new() { Type = type, Carrier = carrier, SwitchState = EPropSwitchState.Enabled });
|
||||
}
|
||||
|
||||
private static LevelState SetFloorProp(LevelState level, GridPosition position, PropState prop)
|
||||
{
|
||||
return level.IsFloor(position) ? level.SetProp(position, prop) : level;
|
||||
}
|
||||
|
||||
private static LevelState SetDoor(LevelState level, GridPosition position)
|
||||
{
|
||||
if (!level.IsFloor(position))
|
||||
return level;
|
||||
|
||||
var neighbor = position.Neighbors().FirstOrDefault(level.IsFloor);
|
||||
if (neighbor is null)
|
||||
return SetFloorProp(level, position, new() { Type = EPropType.Door });
|
||||
|
||||
return SetFloorProp(level, position, new() { Type = EPropType.Door }) with {
|
||||
Doors = [.. level.Doors, new DoorState { A = position, B = neighbor }]
|
||||
};
|
||||
}
|
||||
|
||||
private static LevelState SetReactorControl(LevelState level, GridPosition position)
|
||||
{
|
||||
if (!level.IsFloor(position))
|
||||
return level;
|
||||
|
||||
var id = level.Reactors.Count == 0 ? 1 : level.Reactors.Max(reactor => reactor.ReactorId) + 1;
|
||||
var levelWithProp = level.SetProp(position, new() { Type = EPropType.ReactorControl, ReactorId = id });
|
||||
return levelWithProp with {
|
||||
Reactors = [
|
||||
.. level.Reactors,
|
||||
new ReactorBinding {
|
||||
ReactorId = id,
|
||||
ControlPosition = position,
|
||||
FuelConsumerPosition = position,
|
||||
CoolantConsumerPosition = position,
|
||||
ElectricityConsumerPosition = position
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
private static LevelState SetLeak(LevelState level, GridPosition position, ECarrierType carrier)
|
||||
{
|
||||
if (!level.InBounds(position))
|
||||
return level;
|
||||
|
||||
var accessPosition = carrier == ECarrierType.Electricity && level.GetTerrain(position) == ECellTerrain.Wall
|
||||
? position.Neighbors().FirstOrDefault(level.IsFloor)
|
||||
: position;
|
||||
|
||||
if (accessPosition is null || !level.IsFloor(accessPosition))
|
||||
return level;
|
||||
|
||||
var next = level.SetUnderground(position, carrier, new() { State = EUndergroundState.Leaking });
|
||||
return next with {
|
||||
Leaks = [
|
||||
.. next.Leaks,
|
||||
new LeakState {
|
||||
Carrier = carrier,
|
||||
UndergroundPosition = position,
|
||||
AccessPosition = accessPosition
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -5,28 +5,31 @@ namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public static class LevelSerializer
|
||||
{
|
||||
private const int c_CurrentVersion = 1;
|
||||
private const int c_CurrentVersion = 2;
|
||||
|
||||
public static string Serialize(LevelState level)
|
||||
{
|
||||
return JsonSerializer.Serialize(new LevelFile {
|
||||
Version = c_CurrentVersion,
|
||||
Level = level
|
||||
}, Options);
|
||||
}, s_Options);
|
||||
}
|
||||
|
||||
public static LevelState Deserialize(string json)
|
||||
{
|
||||
var file = JsonSerializer.Deserialize<LevelFile>(json, Options) ?? throw new InvalidOperationException("Level file did not contain a level.");
|
||||
var level = file.Version switch {
|
||||
c_CurrentVersion => file.Level,
|
||||
_ => throw new InvalidOperationException($"Unsupported level file version {file.Version}.")
|
||||
};
|
||||
var file = JsonSerializer.Deserialize<LevelFile>(json, s_Options) ?? throw new InvalidOperationException("Level file did not contain a level.");
|
||||
if (file.Version != c_CurrentVersion)
|
||||
throw new InvalidOperationException($"Unsupported level file version {file.Version}. Expected {c_CurrentVersion}.");
|
||||
|
||||
return level.Cells.Length != level.Width * level.Height ? throw new InvalidOperationException("Level cell count does not match its dimensions.") : level;
|
||||
var level = file.Level ?? throw new InvalidOperationException("Level file did not contain a level.");
|
||||
var report = new LevelValidator().Validate(level);
|
||||
if (!report.IsValid)
|
||||
throw new InvalidOperationException(report.Errors[0].Message);
|
||||
|
||||
return level;
|
||||
}
|
||||
|
||||
private static readonly JsonSerializerOptions Options = new() {
|
||||
private static readonly JsonSerializerOptions s_Options = new() {
|
||||
WriteIndented = true,
|
||||
Converters = { new JsonStringEnumConverter() }
|
||||
};
|
||||
@@ -34,6 +37,6 @@ public static class LevelSerializer
|
||||
private sealed record LevelFile
|
||||
{
|
||||
public int Version { get; init; }
|
||||
public LevelState Level { get; init; } = new();
|
||||
public LevelState? Level { get; init; }
|
||||
}
|
||||
}
|
||||
203
src/ReactorMaintenance.Simulation/LevelValidator.cs
Normal file
203
src/ReactorMaintenance.Simulation/LevelValidator.cs
Normal file
@@ -0,0 +1,203 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed class LevelValidator
|
||||
{
|
||||
public ValidationReport Validate(LevelState level)
|
||||
{
|
||||
var errors = new List<ValidationIssue>();
|
||||
var warnings = new List<ValidationIssue>();
|
||||
|
||||
ValidateDimensions(level, errors);
|
||||
ValidateRobot(level, errors);
|
||||
ValidateCells(level, errors);
|
||||
ValidateDoors(level, errors);
|
||||
ValidateLeaks(level, errors);
|
||||
ValidateReactors(level, errors, warnings);
|
||||
ValidateJunctions(level, errors);
|
||||
ValidateRuleEvents(level, errors);
|
||||
ValidateWarnings(level, warnings);
|
||||
|
||||
return new() { Errors = errors, Warnings = warnings };
|
||||
}
|
||||
|
||||
private static void ValidateDimensions(LevelState level, List<ValidationIssue> errors)
|
||||
{
|
||||
if (level.Width < Balancing.Current.MinimumLevelSize || level.Height < Balancing.Current.MinimumLevelSize)
|
||||
errors.Add(new("Invalid level dimensions."));
|
||||
|
||||
var expected = level.Width * level.Height;
|
||||
if (level.Terrain.Length != expected || level.Fuel.Length != expected || level.Coolant.Length != expected || level.Electricity.Length != expected || level.Surface.Length != expected || level.Props.Length != expected)
|
||||
errors.Add(new("Cell array counts do not match level dimensions."));
|
||||
}
|
||||
|
||||
private static void ValidateRobot(LevelState level, List<ValidationIssue> errors)
|
||||
{
|
||||
if (!level.IsFloor(level.Robot.Position))
|
||||
errors.Add(new("Robot must be in bounds on a floor cell.", level.Robot.Position));
|
||||
}
|
||||
|
||||
private static void ValidateCells(LevelState level, List<ValidationIssue> errors)
|
||||
{
|
||||
for (var y = 0; y < level.Height; y++)
|
||||
{
|
||||
for (var x = 0; x < level.Width; x++)
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
var surface = level.GetSurface(position);
|
||||
var prop = level.GetProp(position);
|
||||
|
||||
if (level.GetTerrain(position) == ECellTerrain.Wall)
|
||||
{
|
||||
if (surface.Fuel > 0 || surface.Coolant > 0 || surface.Electricity > 0 || surface.Heat > 0)
|
||||
errors.Add(new("Wall cell cannot store surface hazards.", position));
|
||||
|
||||
if (prop.Type != EPropType.None)
|
||||
errors.Add(new("Prop must be placed on floor terrain.", position));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateDoors(LevelState level, List<ValidationIssue> errors)
|
||||
{
|
||||
foreach (var door in level.Doors)
|
||||
{
|
||||
if (!level.IsFloor(door.A) || !level.IsFloor(door.B) || door.A.ManhattanDistance(door.B) != 1)
|
||||
errors.Add(new("Door edge must connect two adjacent floor cells.", door.A));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateLeaks(LevelState level, List<ValidationIssue> errors)
|
||||
{
|
||||
foreach (var leak in level.Leaks)
|
||||
{
|
||||
if (!level.InBounds(leak.UndergroundPosition) || !level.IsFloor(leak.AccessPosition))
|
||||
{
|
||||
errors.Add(new("Leak must have valid floor access.", leak.AccessPosition));
|
||||
continue;
|
||||
}
|
||||
|
||||
var underground = level.GetUnderground(leak.UndergroundPosition, leak.Carrier);
|
||||
if (!underground.IsPresent)
|
||||
errors.Add(new("Leak target must point to an underground cell.", leak.UndergroundPosition));
|
||||
|
||||
if (leak.Carrier is ECarrierType.Fuel or ECarrierType.Coolant && leak.UndergroundPosition != leak.AccessPosition)
|
||||
errors.Add(new("Fuel and coolant leaks must use their underground coordinate as access.", leak.AccessPosition));
|
||||
|
||||
if (leak.Carrier == ECarrierType.Electricity && leak.UndergroundPosition.ManhattanDistance(leak.AccessPosition) != 1)
|
||||
errors.Add(new("Electricity leak access must be an adjacent floor face.", leak.AccessPosition));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateReactors(LevelState level, List<ValidationIssue> errors, List<ValidationIssue> warnings)
|
||||
{
|
||||
foreach (var reactor in level.Reactors)
|
||||
{
|
||||
if (!IsProp(level, reactor.ControlPosition, EPropType.ReactorControl))
|
||||
errors.Add(new("Reactor binding control position must point to a reactor control prop.", reactor.ControlPosition));
|
||||
|
||||
ValidateConsumerBinding(level, reactor.FuelConsumerPosition, ECarrierType.Fuel, errors);
|
||||
ValidateConsumerBinding(level, reactor.CoolantConsumerPosition, ECarrierType.Coolant, errors);
|
||||
ValidateConsumerBinding(level, reactor.ElectricityConsumerPosition, ECarrierType.Electricity, errors);
|
||||
|
||||
if (!reactor.Ready)
|
||||
warnings.Add(new("Reactor is initially unready.", reactor.ControlPosition));
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateConsumerBinding(LevelState level, GridPosition position, ECarrierType carrier, List<ValidationIssue> errors)
|
||||
{
|
||||
if (!level.InBounds(position) || level.GetProp(position) is not { Type: EPropType.Consumer } prop || prop.Carrier != carrier)
|
||||
errors.Add(new($"Missing or invalid {carrier} consumer binding.", position));
|
||||
}
|
||||
|
||||
private static void ValidateJunctions(LevelState level, List<ValidationIssue> errors)
|
||||
{
|
||||
for (var y = 0; y < level.Height; y++)
|
||||
{
|
||||
for (var x = 0; x < level.Width; x++)
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
var prop = level.GetProp(position);
|
||||
if (prop.Type is not (EPropType.TJunction or EPropType.CrossJunction))
|
||||
continue;
|
||||
|
||||
var carrierCount = Enum.GetValues<ECarrierType>().Count(carrier => level.GetUnderground(position, carrier).IsPresent);
|
||||
if (carrierCount != 1)
|
||||
errors.Add(new("Junction must regulate exactly one underground carrier.", position));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateRuleEvents(LevelState level, List<ValidationIssue> errors)
|
||||
{
|
||||
foreach (var ruleEvent in level.RuleEvents)
|
||||
{
|
||||
foreach (var effect in ruleEvent.Effects)
|
||||
{
|
||||
if (!level.InBounds(effect.Position) && effect.Kind != ERuleEffectKind.EmitWarning && effect.Kind != ERuleEffectKind.MarkTerminalLoss && effect.Kind != ERuleEffectKind.AddInventory)
|
||||
errors.Add(new("Rule effect target is out of bounds.", effect.Position));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateWarnings(LevelState level, List<ValidationIssue> warnings)
|
||||
{
|
||||
foreach (var carrier in Enum.GetValues<ECarrierType>())
|
||||
{
|
||||
for (var y = 0; y < level.Height; y++)
|
||||
{
|
||||
for (var x = 0; x < level.Width; x++)
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
if (level.GetUnderground(position, carrier).IsPresent && !HasSourcePath(level, position, carrier))
|
||||
warnings.Add(new($"Underground {carrier} cell has no source path.", position));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (var y = 0; y < level.Height; y++)
|
||||
{
|
||||
for (var x = 0; x < level.Width; x++)
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
var prop = level.GetProp(position);
|
||||
if (prop.Type == EPropType.Consumer && prop.SwitchState == EPropSwitchState.Enabled && !HasSourcePath(level, position, prop.Carrier))
|
||||
warnings.Add(new("Enabled consumer is initially starved.", position));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasSourcePath(LevelState level, GridPosition start, ECarrierType carrier)
|
||||
{
|
||||
if (!level.GetUnderground(start, carrier).CarriesFlow)
|
||||
return false;
|
||||
|
||||
var visited = new HashSet<GridPosition>();
|
||||
var open = new Queue<GridPosition>();
|
||||
open.Enqueue(start);
|
||||
visited.Add(start);
|
||||
|
||||
while (open.Count > 0)
|
||||
{
|
||||
var current = open.Dequeue();
|
||||
if (level.GetProp(current) is { Type: EPropType.Flow, Carrier: var sourceCarrier, SwitchState: EPropSwitchState.Enabled } && sourceCarrier == carrier)
|
||||
return true;
|
||||
|
||||
foreach (var next in current.Neighbors().Where(level.InBounds))
|
||||
{
|
||||
if (!visited.Add(next) || !level.GetUnderground(next, carrier).CarriesFlow)
|
||||
continue;
|
||||
|
||||
open.Enqueue(next);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsProp(LevelState level, GridPosition position, EPropType propType)
|
||||
{
|
||||
return level.InBounds(position) && level.GetProp(position).Type == propType;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public enum ECellTerrain
|
||||
{
|
||||
@@ -6,102 +6,351 @@ public enum ECellTerrain
|
||||
Wall
|
||||
}
|
||||
|
||||
public enum ECellProp
|
||||
public enum ECarrierType
|
||||
{
|
||||
None,
|
||||
Reactor,
|
||||
CoolingPump,
|
||||
Generator,
|
||||
PressureRegulator,
|
||||
DiagnosticTerminal,
|
||||
ControlTerminal
|
||||
}
|
||||
|
||||
public enum EPipeMedium
|
||||
{
|
||||
None,
|
||||
Pressure,
|
||||
Fuel,
|
||||
Coolant,
|
||||
Fuel
|
||||
Electricity
|
||||
}
|
||||
|
||||
public enum EFailureKind
|
||||
public enum EUndergroundState
|
||||
{
|
||||
PipeBurst,
|
||||
Ignition,
|
||||
Meltdown,
|
||||
StabilityCollapse,
|
||||
ReactorReady
|
||||
Absent,
|
||||
Intact,
|
||||
Leaking
|
||||
}
|
||||
|
||||
public enum EPropType
|
||||
{
|
||||
None,
|
||||
Flow,
|
||||
Consumer,
|
||||
TJunction,
|
||||
CrossJunction,
|
||||
Door,
|
||||
AllSeeingEyeTerminal,
|
||||
RemedySupply,
|
||||
ReactorControl
|
||||
}
|
||||
|
||||
public enum EPropSwitchState
|
||||
{
|
||||
Disabled,
|
||||
Enabled
|
||||
}
|
||||
|
||||
public enum EConsumerServiceState
|
||||
{
|
||||
Unknown,
|
||||
Disabled,
|
||||
Starved,
|
||||
Supplied,
|
||||
Producing
|
||||
}
|
||||
|
||||
public enum ETJunctionMode
|
||||
{
|
||||
ZeroFour,
|
||||
OneThree,
|
||||
TwoTwo,
|
||||
ThreeOne,
|
||||
FourZero
|
||||
}
|
||||
|
||||
public enum ECrossJunctionMode
|
||||
{
|
||||
ZeroThreeThree,
|
||||
ThreeZeroThree,
|
||||
ThreeThreeZero,
|
||||
TwoTwoTwo
|
||||
}
|
||||
|
||||
public enum EDoorState
|
||||
{
|
||||
Open,
|
||||
Closed
|
||||
}
|
||||
|
||||
public enum ERemedyType
|
||||
{
|
||||
FuelNeutralizer,
|
||||
CoolantNeutralizer,
|
||||
ElectricityNeutralizer,
|
||||
HeatShield
|
||||
}
|
||||
|
||||
public enum ELevelState
|
||||
{
|
||||
Stable,
|
||||
Caution,
|
||||
Critical,
|
||||
Ready,
|
||||
Lost,
|
||||
Won
|
||||
}
|
||||
|
||||
public enum EForecastKind
|
||||
{
|
||||
TerminalLoss,
|
||||
ReactorReady,
|
||||
ConsumerStarved,
|
||||
HazardGrowth,
|
||||
RuleEvent
|
||||
}
|
||||
|
||||
public enum ERuleEventPhase
|
||||
{
|
||||
StartOfSimulation,
|
||||
EndOfTurn
|
||||
}
|
||||
|
||||
public enum ERulePredicateKind
|
||||
{
|
||||
TurnAtLeast,
|
||||
LevelStateIs,
|
||||
PropStateAt,
|
||||
ConsumerStateAt,
|
||||
SurfaceBandAt,
|
||||
RobotAt,
|
||||
AllSeeingEyeUnlocked
|
||||
}
|
||||
|
||||
public enum ERuleEffectKind
|
||||
{
|
||||
StartLeak,
|
||||
WorsenLeak,
|
||||
RepairNetworkCell,
|
||||
DisableNetworkCell,
|
||||
SetPropEnabled,
|
||||
AddSurfaceHazard,
|
||||
AddHeat,
|
||||
AddInventory,
|
||||
MarkTerminalLoss,
|
||||
EmitWarning
|
||||
}
|
||||
|
||||
public enum EBand
|
||||
{
|
||||
Safe,
|
||||
Caution,
|
||||
Critical
|
||||
}
|
||||
|
||||
public enum EPairEffect
|
||||
{
|
||||
Hold,
|
||||
FuelFlow,
|
||||
CoolFlow,
|
||||
ChargeFlow,
|
||||
HeatFlow,
|
||||
HeatFlow2,
|
||||
Warm1,
|
||||
Warm2,
|
||||
Quench1,
|
||||
Quench2,
|
||||
Short1,
|
||||
Short2,
|
||||
Ignite1,
|
||||
Ignite2
|
||||
}
|
||||
|
||||
public sealed record GridPosition(int X, int Y)
|
||||
{
|
||||
public IEnumerable<GridPosition> Neighbors()
|
||||
{
|
||||
yield return new(X - Balancing.Current.NeighborDistance, Y);
|
||||
yield return new(X + Balancing.Current.NeighborDistance, Y);
|
||||
yield return new(X, Y - Balancing.Current.NeighborDistance);
|
||||
yield return new(X, Y + Balancing.Current.NeighborDistance);
|
||||
yield return new(X, Y - 1);
|
||||
yield return new(X + 1, Y);
|
||||
yield return new(X, Y + 1);
|
||||
yield return new(X - 1, Y);
|
||||
}
|
||||
|
||||
public int ManhattanDistance(GridPosition other)
|
||||
{
|
||||
return Math.Abs(X - other.X) + Math.Abs(Y - other.Y);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record HazardState
|
||||
public sealed record UndergroundCell
|
||||
{
|
||||
public HazardState Clamp()
|
||||
public EUndergroundState State { get; init; }
|
||||
public float Amount { get; init; }
|
||||
public float Intensity { get; init; }
|
||||
|
||||
public bool IsPresent => State != EUndergroundState.Absent;
|
||||
public bool CarriesFlow => State is EUndergroundState.Intact or EUndergroundState.Leaking;
|
||||
}
|
||||
|
||||
public sealed record SurfaceState
|
||||
{
|
||||
public float Fuel { get; init; }
|
||||
public float Coolant { get; init; }
|
||||
public float Electricity { get; init; }
|
||||
public float Heat { get; init; }
|
||||
public int FuelBlockTurns { get; init; }
|
||||
public int CoolantBlockTurns { get; init; }
|
||||
public int ElectricityBlockTurns { get; init; }
|
||||
|
||||
public SurfaceState Clamp()
|
||||
{
|
||||
var balancing = Balancing.Current;
|
||||
return this with {
|
||||
Heat = Rules.Clamp(Heat),
|
||||
Smoke = Rules.Clamp(Smoke),
|
||||
FuelVapor = Rules.Clamp(FuelVapor),
|
||||
LiquidFuel = Rules.Clamp(LiquidFuel),
|
||||
CoolantPooling = Rules.Clamp(CoolantPooling),
|
||||
ElectricalCharge = Rules.Clamp(ElectricalCharge),
|
||||
Stability = Rules.Clamp(Stability)
|
||||
Fuel = balancing.ClampValue(Fuel),
|
||||
Coolant = balancing.ClampValue(Coolant),
|
||||
Electricity = balancing.ClampValue(Electricity),
|
||||
Heat = balancing.ClampValue(Heat),
|
||||
FuelBlockTurns = Math.Max(0, FuelBlockTurns),
|
||||
CoolantBlockTurns = Math.Max(0, CoolantBlockTurns),
|
||||
ElectricityBlockTurns = Math.Max(0, ElectricityBlockTurns)
|
||||
};
|
||||
}
|
||||
|
||||
public int Heat { get; init; }
|
||||
public int Smoke { get; init; }
|
||||
public int FuelVapor { get; init; }
|
||||
public int LiquidFuel { get; init; }
|
||||
public int CoolantPooling { get; init; }
|
||||
public int ElectricalCharge { get; init; }
|
||||
public int Stability { get; init; } = Balancing.Current.DefaultHazardStability;
|
||||
public bool Fire { get; init; }
|
||||
public bool Blocks(ECarrierType carrier)
|
||||
{
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => FuelBlockTurns > 0,
|
||||
ECarrierType.Coolant => CoolantBlockTurns > 0,
|
||||
ECarrierType.Electricity => ElectricityBlockTurns > 0,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record CellState
|
||||
public sealed record PropState
|
||||
{
|
||||
public ECellTerrain Terrain { get; init; } = ECellTerrain.Floor;
|
||||
public ECellProp Prop { get; init; }
|
||||
public EPipeMedium Pipe { get; init; }
|
||||
public int Flow { get; init; }
|
||||
public int Pressure { get; init; }
|
||||
public int Integrity { get; init; } = Balancing.Current.DefaultCellIntegrity;
|
||||
public int LeakRate { get; init; }
|
||||
public bool PipeOpen { get; init; } = true;
|
||||
public bool Powered { get; init; }
|
||||
public bool DoorLocked { get; init; }
|
||||
public HazardState Hazards { get; init; } = new();
|
||||
public bool IsWalkable => Terrain != ECellTerrain.Wall;
|
||||
public bool HasPipe => Pipe != EPipeMedium.None;
|
||||
public EPropType Type { get; init; }
|
||||
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 ERemedyType RemedyType { get; init; }
|
||||
public bool Depleted { get; init; }
|
||||
public int ReactorId { get; init; }
|
||||
|
||||
public bool IsEnabled => SwitchState == EPropSwitchState.Enabled;
|
||||
}
|
||||
|
||||
public sealed record DoorState
|
||||
{
|
||||
public GridPosition A { get; init; } = new(0, 0);
|
||||
public GridPosition B { get; init; } = new(0, 0);
|
||||
public EDoorState State { get; init; } = EDoorState.Closed;
|
||||
}
|
||||
|
||||
public sealed record LeakState
|
||||
{
|
||||
public ECarrierType Carrier { get; init; }
|
||||
public GridPosition UndergroundPosition { get; init; } = new(0, 0);
|
||||
public GridPosition AccessPosition { get; init; } = new(0, 0);
|
||||
public bool Repaired { get; init; }
|
||||
}
|
||||
|
||||
public sealed record ReactorBinding
|
||||
{
|
||||
public int ReactorId { get; init; }
|
||||
public GridPosition ControlPosition { get; init; } = new(0, 0);
|
||||
public GridPosition FuelConsumerPosition { get; init; } = new(0, 0);
|
||||
public GridPosition CoolantConsumerPosition { get; init; } = new(0, 0);
|
||||
public GridPosition ElectricityConsumerPosition { get; init; } = new(0, 0);
|
||||
public bool Ready { get; init; }
|
||||
public bool Activated { get; init; }
|
||||
}
|
||||
|
||||
public sealed record RobotState
|
||||
{
|
||||
public GridPosition Position { get; init; } = new(1, 1);
|
||||
public int FuelNeutralizers { get; init; }
|
||||
public int CoolantNeutralizers { get; init; }
|
||||
public int ElectricityNeutralizers { get; init; }
|
||||
public int HeatShields { get; init; }
|
||||
public int HeatImmunitySteps { get; init; }
|
||||
|
||||
public int Count(ERemedyType remedy)
|
||||
{
|
||||
return remedy switch {
|
||||
ERemedyType.FuelNeutralizer => FuelNeutralizers,
|
||||
ERemedyType.CoolantNeutralizer => CoolantNeutralizers,
|
||||
ERemedyType.ElectricityNeutralizer => ElectricityNeutralizers,
|
||||
ERemedyType.HeatShield => HeatShields,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(remedy), remedy, "Unsupported remedy.")
|
||||
};
|
||||
}
|
||||
|
||||
public RobotState Add(ERemedyType remedy, int amount)
|
||||
{
|
||||
return remedy switch {
|
||||
ERemedyType.FuelNeutralizer => this with { FuelNeutralizers = FuelNeutralizers + amount },
|
||||
ERemedyType.CoolantNeutralizer => this with { CoolantNeutralizers = CoolantNeutralizers + amount },
|
||||
ERemedyType.ElectricityNeutralizer => this with { ElectricityNeutralizers = ElectricityNeutralizers + amount },
|
||||
ERemedyType.HeatShield => this with { HeatShields = HeatShields + amount },
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(remedy), remedy, "Unsupported remedy.")
|
||||
};
|
||||
}
|
||||
|
||||
public RobotState Spend(ERemedyType remedy)
|
||||
{
|
||||
return Count(remedy) <= 0 ? this : Add(remedy, -1);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record RulePredicate
|
||||
{
|
||||
public ERulePredicateKind Kind { get; init; }
|
||||
public GridPosition Position { get; init; } = new(0, 0);
|
||||
public int Turn { get; init; }
|
||||
public ELevelState LevelState { get; init; }
|
||||
public EPropSwitchState PropSwitchState { get; init; }
|
||||
public EConsumerServiceState ConsumerServiceState { get; init; }
|
||||
public ECarrierType Carrier { get; init; }
|
||||
public EBand Band { get; init; }
|
||||
public bool BoolValue { get; init; }
|
||||
}
|
||||
|
||||
public sealed record RuleEffect
|
||||
{
|
||||
public ERuleEffectKind Kind { get; init; }
|
||||
public GridPosition Position { get; init; } = new(0, 0);
|
||||
public ECarrierType Carrier { get; init; }
|
||||
public ERemedyType Remedy { get; init; }
|
||||
public float Amount { get; init; }
|
||||
public EPropSwitchState PropSwitchState { get; init; }
|
||||
public string Message { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed record RuleEventState
|
||||
{
|
||||
public string Id { get; init; } = string.Empty;
|
||||
public bool Enabled { get; init; } = true;
|
||||
public bool Repeat { get; init; }
|
||||
public bool Triggered { get; init; }
|
||||
public int Priority { get; init; }
|
||||
public ERuleEventPhase Phase { get; init; }
|
||||
public IReadOnlyList<RulePredicate> Predicates { get; init; } = Array.Empty<RulePredicate>();
|
||||
public IReadOnlyList<RuleEffect> Effects { get; init; } = Array.Empty<RuleEffect>();
|
||||
public string ForecastText { get; init; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed record Forecast(EForecastKind Kind, GridPosition? Position, int Turns, string Message);
|
||||
public sealed record ValidationIssue(string Message, GridPosition? Position = null);
|
||||
|
||||
public sealed record ValidationReport
|
||||
{
|
||||
public IReadOnlyList<ValidationIssue> Errors { get; init; } = Array.Empty<ValidationIssue>();
|
||||
public IReadOnlyList<ValidationIssue> Warnings { get; init; } = Array.Empty<ValidationIssue>();
|
||||
public bool IsValid => Errors.Count == 0;
|
||||
}
|
||||
|
||||
public sealed record GlobalState
|
||||
{
|
||||
public int Turn { get; init; }
|
||||
public int ActionsPerTurn { get; init; } = Balancing.Current.DefaultActionsPerTurn;
|
||||
public int CoreHeat { get; init; } = Balancing.Current.DefaultCoreHeat;
|
||||
public int FacilityStability { get; init; } = Balancing.Current.DefaultFacilityStability;
|
||||
public int Power { get; init; } = Balancing.Current.DefaultPower;
|
||||
public int Cooling { get; init; } = Balancing.Current.DefaultCooling;
|
||||
public bool ReactorActivated { get; init; }
|
||||
public bool Lost { get; init; }
|
||||
public string Status { get; init; } = "STABILIZE SYSTEMS";
|
||||
public int ActionsRemaining { get; init; } = Balancing.Current.ActionsPerTurn;
|
||||
public ELevelState LevelState { get; init; } = ELevelState.Stable;
|
||||
public string Status { get; init; } = "STABLE";
|
||||
public bool AllSeeingEyeUnlocked { get; init; }
|
||||
public bool TerminalLoss { get; init; }
|
||||
public IReadOnlyList<string> Warnings { get; init; } = Array.Empty<string>();
|
||||
}
|
||||
|
||||
public sealed record Forecast(EFailureKind Kind, GridPosition? Position, int Turns, string Message);
|
||||
|
||||
public sealed record LevelState
|
||||
{
|
||||
public static LevelState Create(string name, int width, int height)
|
||||
@@ -109,73 +358,177 @@ public sealed record LevelState
|
||||
if (width < Balancing.Current.MinimumLevelSize || height < Balancing.Current.MinimumLevelSize)
|
||||
throw new ArgumentOutOfRangeException(nameof(width), $"Levels must be at least {Balancing.Current.MinimumLevelSize}x{Balancing.Current.MinimumLevelSize}.");
|
||||
|
||||
var cells = CreateCells(width, height);
|
||||
for (var y = Balancing.Current.FirstGridCoordinate; y < height; y++)
|
||||
var terrain = Enumerable.Repeat(ECellTerrain.Floor, width * height).ToArray();
|
||||
for (var y = 0; y < height; y++)
|
||||
{
|
||||
for (var x = Balancing.Current.FirstGridCoordinate; x < width; x++)
|
||||
for (var x = 0; x < width; x++)
|
||||
{
|
||||
if (x == Balancing.Current.FirstGridCoordinate || y == Balancing.Current.FirstGridCoordinate || x == width - Balancing.Current.NeighborDistance || y == height - Balancing.Current.NeighborDistance)
|
||||
cells[y * width + x] = cells[y * width + x] with { Terrain = ECellTerrain.Wall };
|
||||
if (x == 0 || y == 0 || x == width - 1 || y == height - 1)
|
||||
terrain[y * width + x] = ECellTerrain.Wall;
|
||||
}
|
||||
}
|
||||
|
||||
return new() {
|
||||
var level = new LevelState {
|
||||
Name = name,
|
||||
Width = width,
|
||||
Height = height,
|
||||
Cells = cells,
|
||||
Robot = new(Balancing.Current.DefaultRobotCoordinate, Balancing.Current.DefaultRobotCoordinate)
|
||||
Terrain = terrain,
|
||||
Fuel = CreateUnderground(width, height),
|
||||
Coolant = CreateUnderground(width, height),
|
||||
Electricity = CreateUnderground(width, height),
|
||||
Surface = CreateSurface(width, height),
|
||||
Props = CreateProps(width, height),
|
||||
Robot = new() { Position = new(1, 1) }
|
||||
};
|
||||
}
|
||||
|
||||
public CellState GetCell(GridPosition position)
|
||||
{
|
||||
EnsureInBounds(position);
|
||||
return Cells[Index(position)];
|
||||
}
|
||||
|
||||
public LevelState SetCell(GridPosition position, CellState cell)
|
||||
{
|
||||
EnsureInBounds(position);
|
||||
var cells = Cells.ToArray();
|
||||
cells[Index(position)] = cell;
|
||||
return this with { Cells = cells };
|
||||
return level with { Forecasts = Array.Empty<Forecast>() };
|
||||
}
|
||||
|
||||
public bool InBounds(GridPosition position)
|
||||
{
|
||||
return position.X >= Balancing.Current.FirstGridCoordinate && position.Y >= Balancing.Current.FirstGridCoordinate && position.X < Width && position.Y < Height;
|
||||
return position.X >= 0 && position.Y >= 0 && position.X < Width && position.Y < Height;
|
||||
}
|
||||
|
||||
public int Index(GridPosition position)
|
||||
{
|
||||
EnsureInBounds(position);
|
||||
return position.Y * Width + position.X;
|
||||
}
|
||||
|
||||
public ECellTerrain GetTerrain(GridPosition position)
|
||||
{
|
||||
return Terrain[Index(position)];
|
||||
}
|
||||
|
||||
public UndergroundCell GetUnderground(GridPosition position, ECarrierType carrier)
|
||||
{
|
||||
return Layer(carrier)[Index(position)];
|
||||
}
|
||||
|
||||
public SurfaceState GetSurface(GridPosition position)
|
||||
{
|
||||
return Surface[Index(position)];
|
||||
}
|
||||
|
||||
public PropState GetProp(GridPosition position)
|
||||
{
|
||||
return Props[Index(position)];
|
||||
}
|
||||
|
||||
public bool IsFloor(GridPosition position)
|
||||
{
|
||||
return InBounds(position) && GetTerrain(position) == ECellTerrain.Floor;
|
||||
}
|
||||
|
||||
public bool IsClosedDoorEdge(GridPosition a, GridPosition b)
|
||||
{
|
||||
return Doors.Any(door => door.State == EDoorState.Closed && SameEdge(door.A, door.B, a, b));
|
||||
}
|
||||
|
||||
public LevelState SetTerrain(GridPosition position, ECellTerrain terrain)
|
||||
{
|
||||
var next = Terrain.ToArray();
|
||||
next[Index(position)] = terrain;
|
||||
var level = this with { Terrain = next };
|
||||
return terrain == ECellTerrain.Wall ? level.ClearFloorOnlyState(position) : level;
|
||||
}
|
||||
|
||||
public LevelState SetUnderground(GridPosition position, ECarrierType carrier, UndergroundCell cell)
|
||||
{
|
||||
var next = Layer(carrier).ToArray();
|
||||
next[Index(position)] = cell;
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => this with { Fuel = next },
|
||||
ECarrierType.Coolant => this with { Coolant = next },
|
||||
ECarrierType.Electricity => this with { Electricity = next },
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
|
||||
};
|
||||
}
|
||||
|
||||
public LevelState SetSurface(GridPosition position, SurfaceState surface)
|
||||
{
|
||||
var next = Surface.ToArray();
|
||||
next[Index(position)] = surface.Clamp();
|
||||
return this with { Surface = next };
|
||||
}
|
||||
|
||||
public LevelState SetProp(GridPosition position, PropState prop)
|
||||
{
|
||||
var next = Props.ToArray();
|
||||
next[Index(position)] = prop;
|
||||
return this with { Props = next };
|
||||
}
|
||||
|
||||
public LevelState WithRuntimeArrays(UndergroundCell[] fuel, UndergroundCell[] coolant, UndergroundCell[] electricity, SurfaceState[] surface, PropState[] props)
|
||||
{
|
||||
return this with {
|
||||
Fuel = fuel,
|
||||
Coolant = coolant,
|
||||
Electricity = electricity,
|
||||
Surface = surface,
|
||||
Props = props
|
||||
};
|
||||
}
|
||||
|
||||
public IReadOnlyList<UndergroundCell> Layer(ECarrierType carrier)
|
||||
{
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => Fuel,
|
||||
ECarrierType.Coolant => Coolant,
|
||||
ECarrierType.Electricity => Electricity,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
|
||||
};
|
||||
}
|
||||
|
||||
private LevelState ClearFloorOnlyState(GridPosition position)
|
||||
{
|
||||
return SetSurface(position, new())
|
||||
.SetProp(position, new())
|
||||
.SetUnderground(position, ECarrierType.Fuel, new())
|
||||
.SetUnderground(position, ECarrierType.Coolant, new())
|
||||
.SetUnderground(position, ECarrierType.Electricity, new());
|
||||
}
|
||||
|
||||
private void EnsureInBounds(GridPosition position)
|
||||
{
|
||||
if (!InBounds(position))
|
||||
throw new ArgumentOutOfRangeException(nameof(position), $"Position {position.X},{position.Y} is outside {Width}x{Height}.");
|
||||
}
|
||||
|
||||
private static CellState[] CreateCells(int width, int height)
|
||||
private static bool SameEdge(GridPosition edgeA, GridPosition edgeB, GridPosition a, GridPosition b)
|
||||
{
|
||||
return Enumerable.Range(Balancing.Current.FirstGridCoordinate, width * height).Select(_ => new CellState()).ToArray();
|
||||
return edgeA == a && edgeB == b || edgeA == b && edgeB == a;
|
||||
}
|
||||
|
||||
private static UndergroundCell[] CreateUnderground(int width, int height)
|
||||
{
|
||||
return Enumerable.Range(0, width * height).Select(_ => new UndergroundCell()).ToArray();
|
||||
}
|
||||
|
||||
private static SurfaceState[] CreateSurface(int width, int height)
|
||||
{
|
||||
return Enumerable.Range(0, width * height).Select(_ => new SurfaceState()).ToArray();
|
||||
}
|
||||
|
||||
private static PropState[] CreateProps(int width, int height)
|
||||
{
|
||||
return Enumerable.Range(0, width * height).Select(_ => new PropState()).ToArray();
|
||||
}
|
||||
|
||||
public string Name { get; init; } = "New Reactor";
|
||||
public int Width { get; init; } = Balancing.Current.DefaultLevelWidth;
|
||||
public int Height { get; init; } = Balancing.Current.DefaultLevelHeight;
|
||||
public CellState[] Cells { get; init; } = CreateCells(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
|
||||
public GridPosition Robot { get; init; } = new(Balancing.Current.DefaultRobotCoordinate, Balancing.Current.DefaultRobotCoordinate);
|
||||
public ECellTerrain[] Terrain { get; init; } = Enumerable.Repeat(ECellTerrain.Floor, Balancing.Current.DefaultLevelWidth * Balancing.Current.DefaultLevelHeight).ToArray();
|
||||
public UndergroundCell[] Fuel { get; init; } = CreateUnderground(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
|
||||
public UndergroundCell[] Coolant { get; init; } = CreateUnderground(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
|
||||
public UndergroundCell[] Electricity { get; init; } = CreateUnderground(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
|
||||
public SurfaceState[] Surface { get; init; } = CreateSurface(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
|
||||
public PropState[] Props { get; init; } = CreateProps(Balancing.Current.DefaultLevelWidth, Balancing.Current.DefaultLevelHeight);
|
||||
public IReadOnlyList<DoorState> Doors { get; init; } = Array.Empty<DoorState>();
|
||||
public IReadOnlyList<LeakState> Leaks { get; init; } = Array.Empty<LeakState>();
|
||||
public IReadOnlyList<ReactorBinding> Reactors { get; init; } = Array.Empty<ReactorBinding>();
|
||||
public IReadOnlyList<RuleEventState> RuleEvents { get; init; } = Array.Empty<RuleEventState>();
|
||||
public RobotState Robot { get; init; } = new();
|
||||
public GlobalState Global { get; init; } = new();
|
||||
public IReadOnlyList<Forecast> Forecasts { get; init; } = Array.Empty<Forecast>();
|
||||
}
|
||||
|
||||
internal static class Rules
|
||||
{
|
||||
public static int Clamp(int value)
|
||||
{
|
||||
return Math.Clamp(value, Balancing.Current.MinHazardValue, Balancing.Current.MaxHazardValue);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -1,150 +1,737 @@
|
||||
using ReactorMaintenance.Simulation.Effects;
|
||||
using ReactorMaintenance.Simulation.Hazards;
|
||||
|
||||
namespace ReactorMaintenance.Simulation;
|
||||
|
||||
public sealed class SimulationEngine(IEnumerable<ISimulationEffect> effects, IEnumerable<IAreaSimulationEffect> areaEffects, IEnumerable<Hazard> hazards)
|
||||
public sealed class SimulationEngine
|
||||
{
|
||||
private sealed record ForecastKey(EFailureKind Kind, GridPosition? Position);
|
||||
public LevelState MoveRobot(LevelState level, GridPosition destination)
|
||||
{
|
||||
if (!CanSpendAction(level) || !level.IsFloor(destination) || level.Robot.Position.ManhattanDistance(destination) != 1)
|
||||
return Refuse(level, "MOVE BLOCKED");
|
||||
|
||||
public SimulationEngine()
|
||||
: this(
|
||||
[new PipeLeakEffect(), new MachineEffect(), new FireAndElectricalHazardEffect(), new CellIntegrityEffect()],
|
||||
[new SmokeSpreadEffect()],
|
||||
[new PipeBurstHazard(), new IgnitionHazard(), new MeltdownHazard(), new StabilityCollapseHazard()])
|
||||
return SpendAction(level with {
|
||||
Robot = level.Robot with {
|
||||
Position = destination,
|
||||
HeatImmunitySteps = Math.Max(0, level.Robot.HeatImmunitySteps - 1)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public LevelState InteractProp(LevelState level)
|
||||
{
|
||||
if (!CanSpendAction(level))
|
||||
return Refuse(level, "NO ACTIONS");
|
||||
|
||||
var position = level.Robot.Position;
|
||||
var prop = level.GetProp(position);
|
||||
if (prop.Type == EPropType.None)
|
||||
return Refuse(level, "NO PROP");
|
||||
|
||||
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.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),
|
||||
EPropType.ReactorControl => ActivateReactor(level),
|
||||
_ => level
|
||||
};
|
||||
|
||||
return SpendAction(next);
|
||||
}
|
||||
|
||||
public LevelState InteractLeak(LevelState level, ECarrierType carrier, bool useRemedy)
|
||||
{
|
||||
if (!CanSpendAction(level))
|
||||
return Refuse(level, "NO ACTIONS");
|
||||
|
||||
var leakIndex = level.Leaks.ToList().FindIndex(leak => !leak.Repaired && leak.Carrier == carrier && leak.AccessPosition == level.Robot.Position);
|
||||
if (leakIndex < 0)
|
||||
return Refuse(level, "NO REACHABLE LEAK");
|
||||
|
||||
var leak = level.Leaks[leakIndex];
|
||||
var next = useRemedy ? ApplyElementRemedy(level, leak) : RepairLeak(level, leakIndex, leak);
|
||||
return SpendAction(next);
|
||||
}
|
||||
|
||||
public LevelState ApplyHeatShield(LevelState level)
|
||||
{
|
||||
if (!CanSpendAction(level) || level.Robot.HeatShields <= 0)
|
||||
return Refuse(level, "NO HEAT SHIELD");
|
||||
|
||||
return SpendAction(level with {
|
||||
Robot = level.Robot.Spend(ERemedyType.HeatShield) with { HeatImmunitySteps = Balancing.Current.HeatShieldSteps }
|
||||
});
|
||||
}
|
||||
|
||||
public LevelState ActivateReactor(LevelState level)
|
||||
{
|
||||
var reactorIndex = level.Reactors.ToList().FindIndex(reactor => reactor.ControlPosition == level.Robot.Position);
|
||||
if (reactorIndex < 0)
|
||||
return Refuse(level, "NO REACTOR CONTROL");
|
||||
|
||||
var reactor = level.Reactors[reactorIndex];
|
||||
if (!reactor.Ready)
|
||||
return Refuse(level, "REACTOR NOT READY");
|
||||
|
||||
var reactors = level.Reactors.ToArray();
|
||||
reactors[reactorIndex] = reactor with { Activated = true };
|
||||
return level with {
|
||||
Reactors = reactors,
|
||||
Global = level.Global with { LevelState = ELevelState.Won, Status = "REACTOR ONLINE" }
|
||||
};
|
||||
}
|
||||
|
||||
public LevelState EndTurn(LevelState level)
|
||||
{
|
||||
return ResolveTurn(level with { Global = level.Global with { ActionsRemaining = 0 } });
|
||||
}
|
||||
|
||||
public LevelState AdvanceTurn(LevelState level)
|
||||
{
|
||||
return AdvanceTurn(level, true);
|
||||
return ResolveTurn(level);
|
||||
}
|
||||
|
||||
public IReadOnlyList<Forecast> Forecast(LevelState level)
|
||||
{
|
||||
var forecasts = new List<Forecast>();
|
||||
var seen = new HashSet<ForecastKey>();
|
||||
var forecastLevel = level with { Cells = level.Cells.ToArray(), Forecasts = Array.Empty<Forecast>() };
|
||||
if (forecastLevel.Global.Lost)
|
||||
AddHazardForecasts(forecasts, seen, forecastLevel, Balancing.Current.CurrentForecastTurn);
|
||||
var simulated = CopyForForecast(level);
|
||||
|
||||
AddReactorReadyForecast(forecasts, seen, forecastLevel, Balancing.Current.CurrentForecastTurn);
|
||||
|
||||
if (IsReactorReady(forecastLevel) || forecastLevel.Global.Lost || forecastLevel.Global.ReactorActivated)
|
||||
return forecasts.OrderBy(f => f.Turns).ThenBy(f => f.Message).ToArray();
|
||||
|
||||
for (var step = Balancing.Current.TurnIncrement; step <= Balancing.Current.MaxForecastStepCount; step++)
|
||||
for (var turn = 0; turn <= Balancing.Current.ForecastHorizon; turn++)
|
||||
{
|
||||
forecastLevel = AdvanceTurn(forecastLevel, false);
|
||||
AddHazardForecasts(forecasts, seen, forecastLevel, step);
|
||||
AddReactorReadyForecast(forecasts, seen, forecastLevel, step);
|
||||
|
||||
if (forecastLevel.Global.Lost || IsReactorReady(forecastLevel) || forecastLevel.Global.ReactorActivated)
|
||||
AddForecasts(forecasts, simulated, turn);
|
||||
if (simulated.Global.LevelState is ELevelState.Lost or ELevelState.Ready or ELevelState.Won)
|
||||
break;
|
||||
|
||||
if (turn < Balancing.Current.ForecastHorizon)
|
||||
simulated = ResolveTurn(simulated, false);
|
||||
}
|
||||
|
||||
return forecasts.OrderBy(f => f.Turns).ThenBy(f => f.Message).ToArray();
|
||||
return forecasts.DistinctBy(forecast => (forecast.Kind, forecast.Position, forecast.Message)).OrderBy(forecast => forecast.Turns).ThenBy(forecast => forecast.Message).ToArray();
|
||||
}
|
||||
|
||||
public LevelState ActivateReactor(LevelState level)
|
||||
private LevelState ResolveTurn(LevelState level, bool refreshForecasts = true)
|
||||
{
|
||||
if (!IsReactorReady(level))
|
||||
return level with { Global = level.Global with { Status = "REACTOR NOT READY" } };
|
||||
var report = m_Validator.Validate(level);
|
||||
if (!report.IsValid)
|
||||
return level with { Global = level.Global with { LevelState = ELevelState.Lost, Status = report.Errors[0].Message } };
|
||||
|
||||
return level with {
|
||||
Global = level.Global with {
|
||||
ReactorActivated = true,
|
||||
Status = "REACTOR ONLINE"
|
||||
var next = ApplyRuleEvents(level, ERuleEventPhase.StartOfSimulation);
|
||||
next = PropagateNetworks(next);
|
||||
next = ResolveConsumers(next);
|
||||
next = InjectLeaks(next);
|
||||
next = ResolveSurfaceInteractions(next);
|
||||
next = ResolveRobotSafety(next);
|
||||
next = DeriveReactorAndLevelState(next);
|
||||
next = ApplyRuleEvents(next, ERuleEventPhase.EndOfTurn);
|
||||
next = AdvanceDurations(next);
|
||||
next = next with {
|
||||
Global = next.Global with {
|
||||
Turn = next.Global.Turn + 1,
|
||||
ActionsRemaining = Balancing.Current.ActionsPerTurn
|
||||
}
|
||||
};
|
||||
|
||||
return refreshForecasts ? next with { Forecasts = Forecast(next) } : next;
|
||||
}
|
||||
|
||||
private LevelState PropagateNetworks(LevelState level)
|
||||
{
|
||||
var fuel = ClearTransient(level.Fuel);
|
||||
var coolant = ClearTransient(level.Coolant);
|
||||
var electricity = ClearTransient(level.Electricity);
|
||||
var next = level.WithRuntimeArrays(fuel, coolant, electricity, level.Surface.ToArray(), level.Props.ToArray());
|
||||
|
||||
foreach (var carrier in Enum.GetValues<ECarrierType>())
|
||||
next = PropagateCarrier(next, carrier);
|
||||
|
||||
return next;
|
||||
}
|
||||
|
||||
private static UndergroundCell[] ClearTransient(IReadOnlyList<UndergroundCell> layer)
|
||||
{
|
||||
return layer.Select(cell => cell with { Amount = 0, Intensity = 0 }).ToArray();
|
||||
}
|
||||
|
||||
private LevelState PropagateCarrier(LevelState level, ECarrierType carrier)
|
||||
{
|
||||
var layer = level.Layer(carrier).ToArray();
|
||||
var sources = AllPositions(level).Where(position => level.GetProp(position) is { Type: EPropType.Flow, SwitchState: EPropSwitchState.Enabled, Carrier: var sourceCarrier } && sourceCarrier == carrier && level.GetUnderground(position, carrier).CarriesFlow).ToArray();
|
||||
|
||||
foreach (var source in sources)
|
||||
ApplySourceFlow(level, layer, source, carrier);
|
||||
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => level with { Fuel = layer },
|
||||
ECarrierType.Coolant => level with { Coolant = layer },
|
||||
ECarrierType.Electricity => level with { Electricity = layer },
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
|
||||
};
|
||||
}
|
||||
|
||||
private LevelState AdvanceTurn(LevelState level, bool updateForecasts)
|
||||
private static void ApplySourceFlow(LevelState level, UndergroundCell[] layer, GridPosition source, ECarrierType carrier)
|
||||
{
|
||||
var cells = level.Cells.ToArray();
|
||||
var open = new Queue<(GridPosition Position, int Distance, float AmountFactor, float IntensityFactor)>();
|
||||
var best = new Dictionary<GridPosition, float>();
|
||||
open.Enqueue((source, 0, 1, 1));
|
||||
best[source] = 1;
|
||||
|
||||
for (var y = Balancing.Current.FirstGridCoordinate; y < level.Height; y++)
|
||||
while (open.Count > 0)
|
||||
{
|
||||
for (var x = Balancing.Current.FirstGridCoordinate; x < level.Width; x++)
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
var index = level.Index(position);
|
||||
var cell = cells[index];
|
||||
var current = open.Dequeue();
|
||||
var amount = Balancing.Current.ClampValue((Balancing.Current.SourceAmount * current.AmountFactor) - (current.Distance * Balancing.Current.DistanceAmountFalloff));
|
||||
var intensity = Balancing.Current.ClampValue((Balancing.Current.SourceIntensity * current.IntensityFactor) - (current.Distance * Balancing.Current.DistanceIntensityFalloff));
|
||||
var index = level.Index(current.Position);
|
||||
layer[index] = layer[index] with {
|
||||
Amount = Math.Max(layer[index].Amount, amount),
|
||||
Intensity = Math.Max(layer[index].Intensity, intensity)
|
||||
};
|
||||
|
||||
if (!cell.IsWalkable)
|
||||
foreach (var next in current.Position.Neighbors().Where(level.InBounds))
|
||||
{
|
||||
if (!level.GetUnderground(next, carrier).CarriesFlow)
|
||||
continue;
|
||||
|
||||
foreach (var effect in m_Effects)
|
||||
cell = effect.Apply(cell);
|
||||
var weights = BranchWeights(level, current.Position, next);
|
||||
var amountFactor = current.AmountFactor * weights.Amount;
|
||||
var intensityFactor = current.IntensityFactor * weights.Intensity;
|
||||
if (amountFactor <= 0 || intensityFactor <= 0)
|
||||
continue;
|
||||
|
||||
cells[index] = cell with { Hazards = cell.Hazards.Clamp() };
|
||||
if (best.TryGetValue(next, out var oldBest) && oldBest >= amountFactor)
|
||||
continue;
|
||||
|
||||
best[next] = amountFactor;
|
||||
open.Enqueue((next, current.Distance + 1, amountFactor, intensityFactor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var areaEffect in m_AreaEffects)
|
||||
cells = areaEffect.Apply(level, cells);
|
||||
private static (float Amount, float Intensity) BranchWeights(LevelState level, GridPosition from, GridPosition to)
|
||||
{
|
||||
var prop = level.GetProp(from);
|
||||
return prop.Type switch {
|
||||
EPropType.TJunction => TJunctionWeights(prop.TJunctionMode),
|
||||
EPropType.CrossJunction => CrossJunctionWeights(prop.CrossJunctionMode),
|
||||
_ => (1, 1)
|
||||
};
|
||||
}
|
||||
|
||||
var global = UpdateGlobal(level, cells);
|
||||
var next = level with {
|
||||
Cells = cells,
|
||||
Global = global with { Turn = level.Global.Turn + Balancing.Current.TurnIncrement }
|
||||
private static (float Amount, float Intensity) TJunctionWeights(ETJunctionMode mode)
|
||||
{
|
||||
var weight = mode switch {
|
||||
ETJunctionMode.ZeroFour => 0,
|
||||
ETJunctionMode.OneThree => 0.25f,
|
||||
ETJunctionMode.TwoTwo => 0.5f,
|
||||
ETJunctionMode.ThreeOne => 0.75f,
|
||||
ETJunctionMode.FourZero => 1,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unsupported T-junction mode.")
|
||||
};
|
||||
return (weight, weight);
|
||||
}
|
||||
|
||||
private static (float Amount, float Intensity) CrossJunctionWeights(ECrossJunctionMode mode)
|
||||
{
|
||||
var weight = mode switch {
|
||||
ECrossJunctionMode.ZeroThreeThree => 0,
|
||||
ECrossJunctionMode.ThreeZeroThree => 0.5f,
|
||||
ECrossJunctionMode.ThreeThreeZero => 0.5f,
|
||||
ECrossJunctionMode.TwoTwoTwo => 1f / 3f,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, "Unsupported cross-junction mode.")
|
||||
};
|
||||
return (weight, weight);
|
||||
}
|
||||
|
||||
private LevelState ResolveConsumers(LevelState level)
|
||||
{
|
||||
var props = level.Props.ToArray();
|
||||
foreach (var position in AllPositions(level))
|
||||
{
|
||||
var index = level.Index(position);
|
||||
var prop = props[index];
|
||||
if (prop.Type != EPropType.Consumer)
|
||||
continue;
|
||||
|
||||
if (prop.SwitchState == EPropSwitchState.Disabled)
|
||||
{
|
||||
props[index] = prop with { ServiceState = EConsumerServiceState.Disabled };
|
||||
continue;
|
||||
}
|
||||
|
||||
var underground = level.GetUnderground(position, prop.Carrier);
|
||||
var supplied = underground.Amount >= Balancing.Current.ConsumerRequiredAmount && underground.Intensity >= Balancing.Current.ConsumerRequiredIntensity;
|
||||
props[index] = prop with { ServiceState = supplied ? EConsumerServiceState.Producing : EConsumerServiceState.Starved };
|
||||
}
|
||||
|
||||
return level with { Props = props };
|
||||
}
|
||||
|
||||
private LevelState InjectLeaks(LevelState level)
|
||||
{
|
||||
var surface = level.Surface.ToArray();
|
||||
foreach (var leak in level.Leaks.Where(leak => !leak.Repaired))
|
||||
{
|
||||
var underground = level.GetUnderground(leak.UndergroundPosition, leak.Carrier);
|
||||
if (underground.State != EUndergroundState.Leaking)
|
||||
continue;
|
||||
|
||||
var accessIndex = level.Index(leak.AccessPosition);
|
||||
if (surface[accessIndex].Blocks(leak.Carrier))
|
||||
continue;
|
||||
|
||||
var amount = Balancing.Current.LeakBaseAmount + underground.Amount * Balancing.Current.LeakAmountScale + underground.Intensity * Balancing.Current.LeakIntensityScale;
|
||||
surface[accessIndex] = AddSurfaceCarrier(surface[accessIndex], leak.Carrier, amount);
|
||||
}
|
||||
|
||||
return level with { Surface = surface.Select(cell => cell.Clamp()).ToArray() };
|
||||
}
|
||||
|
||||
private LevelState ResolveSurfaceInteractions(LevelState level)
|
||||
{
|
||||
var deltas = Enumerable.Range(0, level.Width * level.Height).Select(_ => new SurfaceDelta()).ToArray();
|
||||
foreach (var position in AllPositions(level).Where(level.IsFloor))
|
||||
ApplySameCellInteractions(level, position, deltas);
|
||||
|
||||
foreach (var position in AllPositions(level).Where(level.IsFloor))
|
||||
{
|
||||
foreach (var neighbor in position.Neighbors().Where(level.IsFloor))
|
||||
{
|
||||
if (level.Index(position) >= level.Index(neighbor) || level.IsClosedDoorEdge(position, neighbor))
|
||||
continue;
|
||||
|
||||
ApplyAdjacentInteractions(level, position, neighbor, deltas);
|
||||
}
|
||||
}
|
||||
|
||||
var surface = level.Surface.ToArray();
|
||||
for (var i = 0; i < surface.Length; i++)
|
||||
surface[i] = deltas[i].Apply(surface[i]).Clamp();
|
||||
|
||||
return level with { Surface = surface };
|
||||
}
|
||||
|
||||
private static void ApplySameCellInteractions(LevelState level, GridPosition position, SurfaceDelta[] deltas)
|
||||
{
|
||||
var surface = level.GetSurface(position);
|
||||
ApplyPair(level, position, position, ECarrierType.Fuel, BandFuel(surface.Fuel), ECarrierType.Electricity, BandElectricity(surface.Electricity), deltas);
|
||||
ApplyPair(level, position, position, ECarrierType.Fuel, BandFuel(surface.Fuel), null, BandHeat(surface.Heat), deltas);
|
||||
ApplyPair(level, position, position, ECarrierType.Coolant, BandCoolant(surface.Coolant), ECarrierType.Electricity, BandElectricity(surface.Electricity), deltas);
|
||||
ApplyPair(level, position, position, ECarrierType.Coolant, BandCoolant(surface.Coolant), null, BandHeat(surface.Heat), deltas);
|
||||
}
|
||||
|
||||
private static void ApplyAdjacentInteractions(LevelState level, GridPosition a, GridPosition b, SurfaceDelta[] deltas)
|
||||
{
|
||||
var surfaceA = level.GetSurface(a);
|
||||
var surfaceB = level.GetSurface(b);
|
||||
FlowBetween(level, a, b, surfaceA.Fuel, surfaceB.Fuel, EPairEffect.FuelFlow, deltas);
|
||||
FlowBetween(level, a, b, surfaceA.Coolant, surfaceB.Coolant, EPairEffect.CoolFlow, deltas);
|
||||
FlowBetween(level, a, b, surfaceA.Electricity, surfaceB.Electricity, EPairEffect.ChargeFlow, deltas);
|
||||
FlowBetween(level, a, b, surfaceA.Heat, surfaceB.Heat, EPairEffect.HeatFlow, deltas);
|
||||
}
|
||||
|
||||
private static void ApplyPair(LevelState level, GridPosition a, GridPosition b, ECarrierType? rowCarrier, EBand rowBand, ECarrierType? colCarrier, EBand colBand, SurfaceDelta[] deltas)
|
||||
{
|
||||
ApplyEffect(level, a, b, PairEffect(rowCarrier, rowBand, colCarrier, colBand), deltas);
|
||||
}
|
||||
|
||||
private static EPairEffect PairEffect(ECarrierType? rowCarrier, EBand rowBand, ECarrierType? colCarrier, EBand colBand)
|
||||
{
|
||||
if (rowBand == EBand.Safe && colBand == EBand.Safe)
|
||||
return EPairEffect.Hold;
|
||||
|
||||
if (rowCarrier == colCarrier)
|
||||
return rowCarrier switch {
|
||||
ECarrierType.Fuel => EPairEffect.FuelFlow,
|
||||
ECarrierType.Coolant => EPairEffect.CoolFlow,
|
||||
ECarrierType.Electricity => EPairEffect.ChargeFlow,
|
||||
_ => EPairEffect.HeatFlow
|
||||
};
|
||||
|
||||
return updateForecasts ? next with { Forecasts = Forecast(next) } : next;
|
||||
if (rowCarrier == ECarrierType.Fuel && colCarrier == ECarrierType.Electricity)
|
||||
return rowBand == EBand.Critical || colBand == EBand.Critical ? EPairEffect.Ignite2 : EPairEffect.Ignite1;
|
||||
|
||||
if (rowCarrier == ECarrierType.Fuel && colCarrier is null)
|
||||
return rowBand == EBand.Critical || colBand == EBand.Critical ? EPairEffect.Ignite2 : EPairEffect.Warm1;
|
||||
|
||||
if (rowCarrier == ECarrierType.Coolant && colCarrier == ECarrierType.Electricity)
|
||||
return rowBand == EBand.Critical || colBand == EBand.Critical ? EPairEffect.Short2 : EPairEffect.Short1;
|
||||
|
||||
if (rowCarrier == ECarrierType.Coolant && colCarrier is null)
|
||||
return rowBand == EBand.Critical || colBand == EBand.Critical ? EPairEffect.Quench2 : EPairEffect.Quench1;
|
||||
|
||||
return EPairEffect.Hold;
|
||||
}
|
||||
|
||||
private void AddHazardForecasts(List<Forecast> forecasts, HashSet<ForecastKey> seen, LevelState level, int turns)
|
||||
private static void ApplyEffect(LevelState level, GridPosition a, GridPosition b, EPairEffect effect, SurfaceDelta[] deltas)
|
||||
{
|
||||
foreach (var hazard in m_Hazards)
|
||||
var index = level.Index(a);
|
||||
switch (effect)
|
||||
{
|
||||
foreach (var forecast in hazard.Predict(level, turns))
|
||||
AddForecast(forecasts, seen, forecast);
|
||||
case EPairEffect.Warm1:
|
||||
deltas[index].Heat += Balancing.Current.Warm1Amount;
|
||||
break;
|
||||
case EPairEffect.Warm2:
|
||||
deltas[index].Heat += Balancing.Current.Warm2Amount;
|
||||
break;
|
||||
case EPairEffect.Quench1:
|
||||
deltas[index].Heat -= Balancing.Current.Quench1Amount;
|
||||
break;
|
||||
case EPairEffect.Quench2:
|
||||
deltas[index].Heat -= Balancing.Current.Quench2Amount;
|
||||
break;
|
||||
case EPairEffect.Short1:
|
||||
deltas[index].Heat += Balancing.Current.Short1Heat;
|
||||
deltas[index].Electricity -= Balancing.Current.Short1Discharge;
|
||||
break;
|
||||
case EPairEffect.Short2:
|
||||
deltas[index].Heat += Balancing.Current.Short2Heat;
|
||||
deltas[index].Electricity -= Balancing.Current.Short2Discharge;
|
||||
break;
|
||||
case EPairEffect.Ignite1:
|
||||
deltas[index].Heat += Balancing.Current.Ignite1Heat;
|
||||
deltas[index].Fuel -= Balancing.Current.Ignite1FuelConsumption;
|
||||
break;
|
||||
case EPairEffect.Ignite2:
|
||||
deltas[index].Heat += Balancing.Current.Ignite2Heat;
|
||||
deltas[index].Fuel -= Balancing.Current.Ignite2FuelConsumption;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddReactorReadyForecast(List<Forecast> forecasts, HashSet<ForecastKey> seen, LevelState level, int turns)
|
||||
private static void FlowBetween(LevelState level, GridPosition a, GridPosition b, float valueA, float valueB, EPairEffect effect, SurfaceDelta[] deltas)
|
||||
{
|
||||
if (IsReactorReady(level))
|
||||
AddForecast(forecasts, seen, new(EFailureKind.ReactorReady, null, turns, "REACTOR READY"));
|
||||
var difference = valueA - valueB;
|
||||
if (Math.Abs(difference) < 0.01f)
|
||||
return;
|
||||
|
||||
var amount = difference * (effect == EPairEffect.HeatFlow2 ? Balancing.Current.StrongFlowTransferRatio : Balancing.Current.FlowTransferRatio);
|
||||
var indexA = level.Index(a);
|
||||
var indexB = level.Index(b);
|
||||
|
||||
switch (effect)
|
||||
{
|
||||
case EPairEffect.FuelFlow:
|
||||
deltas[indexA].Fuel -= amount;
|
||||
deltas[indexB].Fuel += amount;
|
||||
break;
|
||||
case EPairEffect.CoolFlow:
|
||||
deltas[indexA].Coolant -= amount;
|
||||
deltas[indexB].Coolant += amount;
|
||||
break;
|
||||
case EPairEffect.ChargeFlow:
|
||||
deltas[indexA].Electricity -= amount;
|
||||
deltas[indexB].Electricity += amount;
|
||||
break;
|
||||
case EPairEffect.HeatFlow:
|
||||
case EPairEffect.HeatFlow2:
|
||||
deltas[indexA].Heat -= amount;
|
||||
deltas[indexB].Heat += amount;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddForecast(List<Forecast> forecasts, HashSet<ForecastKey> seen, Forecast forecast)
|
||||
private LevelState ResolveRobotSafety(LevelState level)
|
||||
{
|
||||
if (seen.Add(new(forecast.Kind, forecast.Position)))
|
||||
forecasts.Add(forecast);
|
||||
var surface = level.GetSurface(level.Robot.Position);
|
||||
var unsafeElement = surface.Fuel >= Balancing.Current.RobotFuelSafetyThreshold || surface.Coolant >= Balancing.Current.RobotCoolantSafetyThreshold || surface.Electricity >= Balancing.Current.RobotElectricitySafetyThreshold;
|
||||
var unsafeHeat = surface.Heat >= Balancing.Current.RobotHeatSafetyThreshold && level.Robot.HeatImmunitySteps <= 0;
|
||||
return unsafeElement || unsafeHeat
|
||||
? level with { Global = level.Global with { LevelState = ELevelState.Lost, TerminalLoss = true, Status = "ROBOT LOST" } }
|
||||
: level;
|
||||
}
|
||||
|
||||
private static GlobalState UpdateGlobal(LevelState level, CellState[] cells)
|
||||
private LevelState DeriveReactorAndLevelState(LevelState level)
|
||||
{
|
||||
var reactorHeat = cells.Where(c => c.Prop == ECellProp.Reactor).Select(c => c.Hazards.Heat).DefaultIfEmpty(level.Global.CoreHeat).Max();
|
||||
var poweredGenerators = cells.Count(c => c is { Prop: ECellProp.Generator, Powered: true, Hazards.Fire: false });
|
||||
var poweredPumps = cells.Count(c => c is { Prop: ECellProp.CoolingPump, Powered: true, Hazards.Fire: false });
|
||||
var damagedCriticalCells = cells.Count(c => c.Prop is ECellProp.Reactor or ECellProp.Generator or ECellProp.CoolingPump && c.Hazards.Stability <= Balancing.Current.CriticalCellStabilityThreshold);
|
||||
var stability = Rules.Clamp(level.Global.FacilityStability - damagedCriticalCells);
|
||||
var lost = reactorHeat >= Balancing.Current.MeltdownCoreHeatThreshold || stability <= Balancing.Current.StabilityCollapseThreshold;
|
||||
var status = lost ? reactorHeat >= Balancing.Current.MeltdownCoreHeatThreshold ? "CORE MELTDOWN" : "FACILITY STABILITY COLLAPSE" : "STABILIZE SYSTEMS";
|
||||
var global = level.Global with {
|
||||
CoreHeat = Rules.Clamp(reactorHeat - poweredPumps),
|
||||
Power = Rules.Clamp(poweredGenerators * Balancing.Current.GeneratorPowerOutput),
|
||||
Cooling = Rules.Clamp(poweredPumps * Balancing.Current.CoolingPumpOutput),
|
||||
FacilityStability = stability,
|
||||
Lost = lost,
|
||||
Status = status
|
||||
if (level.Global.LevelState is ELevelState.Lost or ELevelState.Won)
|
||||
return level;
|
||||
|
||||
var reactors = level.Reactors.Select(reactor => reactor with { Ready = IsReactorReady(level, reactor) }).ToArray();
|
||||
if (reactors.Any(reactor => reactor.Ready))
|
||||
return level with { Reactors = reactors, Global = level.Global with { LevelState = ELevelState.Ready, Status = "REACTOR READY" } };
|
||||
|
||||
var maxHeat = level.Surface.Where((_, index) => level.Terrain[index] == ECellTerrain.Floor).Select(surface => surface.Heat).DefaultIfEmpty(0).Max();
|
||||
if (maxHeat >= Balancing.Current.TerminalHeat)
|
||||
return level with { Reactors = reactors, Global = level.Global with { LevelState = ELevelState.Lost, TerminalLoss = true, Status = "REACTOR HEAT TERMINAL" } };
|
||||
|
||||
var hasCritical = level.Surface.Any(surface => BandFuel(surface.Fuel) == EBand.Critical || BandCoolant(surface.Coolant) == EBand.Critical || BandElectricity(surface.Electricity) == EBand.Critical || BandHeat(surface.Heat) == EBand.Critical);
|
||||
var hasCaution = hasCritical || level.Props.Any(prop => prop.ServiceState is EConsumerServiceState.Starved or EConsumerServiceState.Disabled) || level.Leaks.Any(leak => !leak.Repaired);
|
||||
var state = hasCritical ? ELevelState.Critical :
|
||||
hasCaution ? ELevelState.Caution : ELevelState.Stable;
|
||||
return level with { Reactors = reactors, Global = level.Global with { LevelState = state, Status = state.ToString().ToUpperInvariant() } };
|
||||
}
|
||||
|
||||
private static bool IsReactorReady(LevelState level, ReactorBinding reactor)
|
||||
{
|
||||
return HasProducingConsumer(level, reactor.FuelConsumerPosition, ECarrierType.Fuel)
|
||||
&& HasProducingConsumer(level, reactor.CoolantConsumerPosition, ECarrierType.Coolant)
|
||||
&& HasProducingConsumer(level, reactor.ElectricityConsumerPosition, ECarrierType.Electricity)
|
||||
&& level.GetSurface(reactor.ControlPosition).Heat < Balancing.Current.TerminalHeat;
|
||||
}
|
||||
|
||||
private static bool HasProducingConsumer(LevelState level, GridPosition position, ECarrierType carrier)
|
||||
{
|
||||
return level.InBounds(position) && level.GetProp(position) is { Type: EPropType.Consumer, Carrier: var consumerCarrier, ServiceState: EConsumerServiceState.Producing } && consumerCarrier == carrier;
|
||||
}
|
||||
|
||||
private LevelState ApplyRuleEvents(LevelState level, ERuleEventPhase phase)
|
||||
{
|
||||
var next = level;
|
||||
var events = level.RuleEvents.Select((ruleEvent, index) => (Event: ruleEvent, Index: index)).Where(item => item.Event.Enabled && item.Event.Phase == phase && (item.Event.Repeat || !item.Event.Triggered)).OrderBy(item => item.Event.Priority).ToArray();
|
||||
var ruleEvents = next.RuleEvents.ToArray();
|
||||
|
||||
foreach (var item in events)
|
||||
{
|
||||
if (!item.Event.Predicates.All(predicate => PredicateMatches(next, predicate)))
|
||||
continue;
|
||||
|
||||
foreach (var effect in item.Event.Effects)
|
||||
next = ApplyRuleEffect(next, effect);
|
||||
|
||||
ruleEvents[item.Index] = item.Event with { Triggered = true };
|
||||
}
|
||||
|
||||
return next with { RuleEvents = ruleEvents };
|
||||
}
|
||||
|
||||
private static bool PredicateMatches(LevelState level, RulePredicate predicate)
|
||||
{
|
||||
return predicate.Kind switch {
|
||||
ERulePredicateKind.TurnAtLeast => level.Global.Turn >= predicate.Turn,
|
||||
ERulePredicateKind.LevelStateIs => level.Global.LevelState == predicate.LevelState,
|
||||
ERulePredicateKind.PropStateAt => level.InBounds(predicate.Position) && level.GetProp(predicate.Position).SwitchState == predicate.PropSwitchState,
|
||||
ERulePredicateKind.ConsumerStateAt => level.InBounds(predicate.Position) && level.GetProp(predicate.Position).ServiceState == predicate.ConsumerServiceState,
|
||||
ERulePredicateKind.SurfaceBandAt => level.InBounds(predicate.Position) && SurfaceBand(level.GetSurface(predicate.Position), predicate.Carrier) >= predicate.Band,
|
||||
ERulePredicateKind.RobotAt => level.Robot.Position == predicate.Position,
|
||||
ERulePredicateKind.AllSeeingEyeUnlocked => level.Global.AllSeeingEyeUnlocked == predicate.BoolValue,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
private static LevelState ApplyRuleEffect(LevelState level, RuleEffect effect)
|
||||
{
|
||||
return effect.Kind switch {
|
||||
ERuleEffectKind.StartLeak => StartLeak(level, effect),
|
||||
ERuleEffectKind.WorsenLeak => level.SetUnderground(effect.Position, effect.Carrier, level.GetUnderground(effect.Position, effect.Carrier) with { State = EUndergroundState.Leaking }),
|
||||
ERuleEffectKind.RepairNetworkCell => level.SetUnderground(effect.Position, effect.Carrier, level.GetUnderground(effect.Position, effect.Carrier) with { State = EUndergroundState.Intact }),
|
||||
ERuleEffectKind.DisableNetworkCell => level.SetUnderground(effect.Position, effect.Carrier, new()),
|
||||
ERuleEffectKind.SetPropEnabled => level.SetProp(effect.Position, level.GetProp(effect.Position) with { SwitchState = effect.PropSwitchState }),
|
||||
ERuleEffectKind.AddSurfaceHazard => level.SetSurface(effect.Position, AddSurfaceCarrier(level.GetSurface(effect.Position), effect.Carrier, effect.Amount)),
|
||||
ERuleEffectKind.AddHeat => level.SetSurface(effect.Position, level.GetSurface(effect.Position) with { Heat = level.GetSurface(effect.Position).Heat + effect.Amount }),
|
||||
ERuleEffectKind.AddInventory => level with { Robot = level.Robot.Add(effect.Remedy, (int)effect.Amount) },
|
||||
ERuleEffectKind.MarkTerminalLoss => level with { Global = level.Global with { LevelState = ELevelState.Lost, TerminalLoss = true, Status = string.IsNullOrWhiteSpace(effect.Message) ? "TERMINAL FAILURE" : effect.Message } },
|
||||
ERuleEffectKind.EmitWarning => level with { Global = level.Global with { Warnings = [.. level.Global.Warnings, effect.Message] } },
|
||||
_ => level
|
||||
};
|
||||
}
|
||||
|
||||
private static LevelState StartLeak(LevelState level, RuleEffect effect)
|
||||
{
|
||||
var leak = new LeakState {
|
||||
Carrier = effect.Carrier,
|
||||
UndergroundPosition = effect.Position,
|
||||
AccessPosition = effect.Position
|
||||
};
|
||||
return level.SetUnderground(effect.Position, effect.Carrier, level.GetUnderground(effect.Position, effect.Carrier) with { State = EUndergroundState.Leaking }) with { Leaks = [.. level.Leaks, leak] };
|
||||
}
|
||||
|
||||
private LevelState AdvanceDurations(LevelState level)
|
||||
{
|
||||
var surface = level.Surface.Select(cell => cell with {
|
||||
FuelBlockTurns = Math.Max(0, cell.FuelBlockTurns - 1),
|
||||
CoolantBlockTurns = Math.Max(0, cell.CoolantBlockTurns - 1),
|
||||
ElectricityBlockTurns = Math.Max(0, cell.ElectricityBlockTurns - 1)
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
return level with { Surface = surface };
|
||||
}
|
||||
|
||||
private static LevelState ToggleProp(LevelState level, GridPosition position, PropState prop)
|
||||
{
|
||||
var switchState = prop.SwitchState == EPropSwitchState.Enabled ? EPropSwitchState.Disabled : EPropSwitchState.Enabled;
|
||||
return level.SetProp(position, prop with { SwitchState = switchState });
|
||||
}
|
||||
|
||||
private static LevelState ToggleDoor(LevelState level, GridPosition position)
|
||||
{
|
||||
var doors = level.Doors.ToArray();
|
||||
var index = Array.FindIndex(doors, door => door.A == position || door.B == position);
|
||||
if (index < 0)
|
||||
return level;
|
||||
|
||||
doors[index] = doors[index] with { State = doors[index].State == EDoorState.Open ? EDoorState.Closed : EDoorState.Open };
|
||||
return level with { Doors = doors };
|
||||
}
|
||||
|
||||
private static LevelState PickUpRemedy(LevelState level, GridPosition position, PropState prop)
|
||||
{
|
||||
if (prop.Depleted || level.Robot.Count(prop.RemedyType) >= Balancing.Current.InventoryCapacityPerRemedy)
|
||||
return level;
|
||||
|
||||
return level.SetProp(position, prop with { Depleted = true }) with { Robot = level.Robot.Add(prop.RemedyType, 1) };
|
||||
}
|
||||
|
||||
private static LevelState RepairLeak(LevelState level, int leakIndex, LeakState leak)
|
||||
{
|
||||
var leaks = level.Leaks.ToArray();
|
||||
leaks[leakIndex] = leak with { Repaired = true };
|
||||
return level.SetUnderground(leak.UndergroundPosition, leak.Carrier, level.GetUnderground(leak.UndergroundPosition, leak.Carrier) with { State = EUndergroundState.Intact }) with { Leaks = leaks };
|
||||
}
|
||||
|
||||
private static LevelState ApplyElementRemedy(LevelState level, LeakState leak)
|
||||
{
|
||||
var remedy = leak.Carrier switch {
|
||||
ECarrierType.Fuel => ERemedyType.FuelNeutralizer,
|
||||
ECarrierType.Coolant => ERemedyType.CoolantNeutralizer,
|
||||
ECarrierType.Electricity => ERemedyType.ElectricityNeutralizer,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(leak), leak.Carrier, "Unsupported leak carrier.")
|
||||
};
|
||||
|
||||
return IsReactorReady(level with { Cells = cells, Global = global }) ? global with { Status = "REACTOR READY" } : global;
|
||||
if (level.Robot.Count(remedy) <= 0)
|
||||
return Refuse(level, "NO REMEDY");
|
||||
|
||||
var surface = RemoveSurfaceCarrier(level.GetSurface(leak.AccessPosition), leak.Carrier);
|
||||
return level.SetSurface(leak.AccessPosition, surface) with { Robot = level.Robot.Spend(remedy) };
|
||||
}
|
||||
|
||||
private static bool IsReactorReady(LevelState level)
|
||||
private static SurfaceState AddSurfaceCarrier(SurfaceState surface, ECarrierType carrier, float amount)
|
||||
{
|
||||
var hasReactor = level.Cells.Any(c => c.Prop == ECellProp.Reactor);
|
||||
var hasStablePower = level.Global.Power >= Balancing.Current.ReactorReadyPowerThreshold || level.Cells.Any(c => c is { Prop: ECellProp.Generator, Powered: true, Hazards.Fire: false });
|
||||
var hasCooling = level.Global.Cooling >= Balancing.Current.ReactorReadyCoolingThreshold || level.Cells.Any(c => c is { Prop: ECellProp.CoolingPump, Powered: true, Hazards.Fire: false });
|
||||
var reactorStable = level.Global.CoreHeat < Balancing.Current.ReactorReadyCoreHeatThreshold;
|
||||
return hasReactor && hasStablePower && hasCooling && reactorStable && !level.Global.Lost;
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => surface with { Fuel = surface.Fuel + amount },
|
||||
ECarrierType.Coolant => surface with { Coolant = surface.Coolant + amount },
|
||||
ECarrierType.Electricity => surface with { Electricity = surface.Electricity + amount },
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
|
||||
};
|
||||
}
|
||||
|
||||
private readonly IReadOnlyList<IAreaSimulationEffect> m_AreaEffects = areaEffects.ToArray();
|
||||
private readonly IReadOnlyList<ISimulationEffect> m_Effects = effects.ToArray();
|
||||
private readonly IReadOnlyList<Hazard> m_Hazards = hazards.ToArray();
|
||||
private static SurfaceState RemoveSurfaceCarrier(SurfaceState surface, ECarrierType carrier)
|
||||
{
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => surface with { Fuel = 0, FuelBlockTurns = Balancing.Current.RemedyBlockTurns },
|
||||
ECarrierType.Coolant => surface with { Coolant = 0, CoolantBlockTurns = Balancing.Current.RemedyBlockTurns },
|
||||
ECarrierType.Electricity => surface with { Electricity = 0, ElectricityBlockTurns = Balancing.Current.RemedyBlockTurns },
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
|
||||
};
|
||||
}
|
||||
|
||||
private LevelState SpendAction(LevelState level)
|
||||
{
|
||||
var actions = Math.Max(0, level.Global.ActionsRemaining - 1);
|
||||
var next = level with { Global = level.Global with { ActionsRemaining = actions } };
|
||||
return actions == 0 ? ResolveTurn(next) : next;
|
||||
}
|
||||
|
||||
private static bool CanSpendAction(LevelState level)
|
||||
{
|
||||
return level.Global.LevelState is not (ELevelState.Lost or ELevelState.Won) && level.Global.ActionsRemaining > 0;
|
||||
}
|
||||
|
||||
private static LevelState Refuse(LevelState level, string message)
|
||||
{
|
||||
return level with { Global = level.Global with { Status = message } };
|
||||
}
|
||||
|
||||
private static ETJunctionMode NextTJunctionMode(ETJunctionMode mode)
|
||||
{
|
||||
return mode == ETJunctionMode.FourZero ? ETJunctionMode.ZeroFour : mode + 1;
|
||||
}
|
||||
|
||||
private static ECrossJunctionMode NextCrossJunctionMode(ECrossJunctionMode mode)
|
||||
{
|
||||
return mode == ECrossJunctionMode.TwoTwoTwo ? ECrossJunctionMode.ZeroThreeThree : mode + 1;
|
||||
}
|
||||
|
||||
private static EBand SurfaceBand(SurfaceState surface, ECarrierType carrier)
|
||||
{
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => BandFuel(surface.Fuel),
|
||||
ECarrierType.Coolant => BandCoolant(surface.Coolant),
|
||||
ECarrierType.Electricity => BandElectricity(surface.Electricity),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(carrier), carrier, "Unsupported carrier.")
|
||||
};
|
||||
}
|
||||
|
||||
private static EBand BandFuel(float value)
|
||||
{
|
||||
return Balancing.Current.Band(value, Balancing.Current.FuelCaution, Balancing.Current.FuelCritical);
|
||||
}
|
||||
|
||||
private static EBand BandCoolant(float value)
|
||||
{
|
||||
return Balancing.Current.Band(value, Balancing.Current.CoolantCaution, Balancing.Current.CoolantCritical);
|
||||
}
|
||||
|
||||
private static EBand BandElectricity(float value)
|
||||
{
|
||||
return Balancing.Current.Band(value, Balancing.Current.ElectricityCaution, Balancing.Current.ElectricityCritical);
|
||||
}
|
||||
|
||||
private static EBand BandHeat(float value)
|
||||
{
|
||||
return Balancing.Current.Band(value, Balancing.Current.HeatCaution, Balancing.Current.HeatCritical);
|
||||
}
|
||||
|
||||
private static LevelState CopyForForecast(LevelState level)
|
||||
{
|
||||
return level with {
|
||||
Terrain = level.Terrain.ToArray(),
|
||||
Fuel = level.Fuel.ToArray(),
|
||||
Coolant = level.Coolant.ToArray(),
|
||||
Electricity = level.Electricity.ToArray(),
|
||||
Surface = level.Surface.ToArray(),
|
||||
Props = level.Props.ToArray(),
|
||||
Forecasts = Array.Empty<Forecast>()
|
||||
};
|
||||
}
|
||||
|
||||
private static void AddForecasts(List<Forecast> forecasts, LevelState level, int turn)
|
||||
{
|
||||
if (level.Global.LevelState == ELevelState.Lost)
|
||||
forecasts.Add(new(EForecastKind.TerminalLoss, level.Robot.Position, turn, level.Global.Status));
|
||||
|
||||
if (level.Global.LevelState == ELevelState.Ready)
|
||||
forecasts.Add(new(EForecastKind.ReactorReady, null, turn, "REACTOR READY"));
|
||||
|
||||
foreach (var position in AllPositions(level))
|
||||
{
|
||||
var prop = level.GetProp(position);
|
||||
if (prop.Type == EPropType.Consumer && prop.ServiceState == EConsumerServiceState.Starved)
|
||||
forecasts.Add(new(EForecastKind.ConsumerStarved, position, turn, $"{prop.Carrier} consumer starved"));
|
||||
|
||||
var surface = level.GetSurface(position);
|
||||
if (BandFuel(surface.Fuel) == EBand.Critical || BandCoolant(surface.Coolant) == EBand.Critical || BandElectricity(surface.Electricity) == EBand.Critical || BandHeat(surface.Heat) == EBand.Critical)
|
||||
forecasts.Add(new(EForecastKind.HazardGrowth, position, turn, "Critical hazard"));
|
||||
}
|
||||
|
||||
foreach (var ruleEvent in level.RuleEvents.Where(ruleEvent => ruleEvent.Enabled && !string.IsNullOrWhiteSpace(ruleEvent.ForecastText) && ruleEvent.Predicates.All(predicate => PredicateMatches(level, predicate))))
|
||||
forecasts.Add(new(EForecastKind.RuleEvent, null, turn, ruleEvent.ForecastText));
|
||||
}
|
||||
|
||||
private static IEnumerable<GridPosition> AllPositions(LevelState level)
|
||||
{
|
||||
for (var y = 0; y < level.Height; y++)
|
||||
{
|
||||
for (var x = 0; x < level.Width; x++)
|
||||
yield return new(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class SurfaceDelta
|
||||
{
|
||||
public SurfaceState Apply(SurfaceState surface)
|
||||
{
|
||||
return surface with {
|
||||
Fuel = surface.Fuel + Fuel,
|
||||
Coolant = surface.Coolant + Coolant,
|
||||
Electricity = surface.Electricity + Electricity,
|
||||
Heat = surface.Heat + Heat
|
||||
};
|
||||
}
|
||||
|
||||
public float Fuel { get; set; }
|
||||
public float Coolant { get; set; }
|
||||
public float Electricity { get; set; }
|
||||
public float Heat { get; set; }
|
||||
}
|
||||
|
||||
private readonly LevelValidator m_Validator = new();
|
||||
}
|
||||
@@ -15,7 +15,9 @@
|
||||
<AppBarButton Icon="OpenFile" Label="Open" Click="Open_Click" />
|
||||
<AppBarButton Icon="Save" Label="Save" Click="Save_Click" />
|
||||
<AppBarSeparator />
|
||||
<AppBarButton Icon="Play" Label="Simulate" Click="Simulate_Click" />
|
||||
<AppBarButton Icon="Play" Label="End Turn" Click="EndTurn_Click" />
|
||||
<AppBarButton Label="Interact" Click="Interact_Click" />
|
||||
<AppBarButton Label="Heat Shield" Click="HeatShield_Click" />
|
||||
<AppBarButton Icon="Accept" Label="Activate" Click="Activate_Click" />
|
||||
</CommandBar>
|
||||
|
||||
@@ -39,14 +41,17 @@
|
||||
<DataTemplate>
|
||||
<ToggleButton IsChecked="{Binding IsSelected, Mode=TwoWay}"
|
||||
Checked="ToolToggle_Checked" ToolTipService.ToolTip="{Binding Label}"
|
||||
Padding="5" Margin="0,0,8,8">
|
||||
<Image Width="96" Height="96" Source="{Binding Icon}" Stretch="Uniform" />
|
||||
Width="112" MinHeight="46" Padding="6" Margin="0,0,8,8">
|
||||
<TextBlock Text="{Binding Label}" TextWrapping="WrapWholeWords" TextAlignment="Center"
|
||||
FontSize="12" />
|
||||
</ToggleButton>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
<TextBlock Text="Brush applies to the selected cell." Foreground="#9EA7AE" TextWrapping="Wrap" />
|
||||
<TextBlock Text="Left click paints. Use Robot to set the start position." Foreground="#9EA7AE"
|
||||
<TextBlock Text="Left click selects or paints. Right click clears surface prop and hazards."
|
||||
Foreground="#9EA7AE" TextWrapping="Wrap" />
|
||||
<TextBlock Text="Door chooses the first adjacent floor edge. Reactor controls auto-bind to the first available consumers."
|
||||
Foreground="#9EA7AE"
|
||||
TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
@@ -95,15 +100,7 @@
|
||||
<DataTemplate>
|
||||
<Border BorderBrush="#46515A" BorderThickness="1" Padding="8" Margin="0,0,0,8"
|
||||
CornerRadius="3">
|
||||
<Grid ColumnSpacing="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="32" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Image Width="28" Height="28" Source="{Binding Icon}" />
|
||||
<TextBlock Grid.Column="1" Text="{Binding Message}" Foreground="#F4F1E8"
|
||||
TextWrapping="Wrap" />
|
||||
</Grid>
|
||||
<TextBlock Text="{Binding Message}" Foreground="#F4F1E8" TextWrapping="Wrap" />
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
using Microsoft.Graphics.Canvas;
|
||||
using Microsoft.Graphics.Canvas.UI;
|
||||
using Microsoft.Graphics.Canvas;
|
||||
using Microsoft.Graphics.Canvas.Text;
|
||||
using Microsoft.Graphics.Canvas.UI.Xaml;
|
||||
using Microsoft.UI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using ReactorMaintenance.Simulation;
|
||||
using System.Globalization;
|
||||
using Windows.Foundation;
|
||||
@@ -21,23 +20,17 @@ public sealed partial class MainWindow
|
||||
{
|
||||
private sealed record CanvasLayout(double CellSize, double OriginX, double OriginY)
|
||||
{
|
||||
public Rect CellRect(int x, int y)
|
||||
public Rect CellRect(GridPosition position)
|
||||
{
|
||||
return new(OriginX + (x * CellSize), OriginY + (y * CellSize), CellSize, CellSize);
|
||||
}
|
||||
|
||||
public Rect DualTileRect(int x, int y)
|
||||
{
|
||||
return new(OriginX + ((x - 0.5) * CellSize), OriginY + ((y - 0.5) * CellSize), CellSize, CellSize);
|
||||
return new(OriginX + position.X * CellSize, OriginY + position.Y * CellSize, CellSize, CellSize);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record ForecastViewModel(BitmapImage Icon, string Message);
|
||||
private sealed record ForecastViewModel(string Message);
|
||||
|
||||
private sealed class EditorToolViewModel(EEditorTool tool, BitmapImage? icon, string label)
|
||||
private sealed class EditorToolViewModel(EEditorTool tool, string label)
|
||||
{
|
||||
public EEditorTool Tool { get; } = tool;
|
||||
public BitmapImage? Icon { get; } = icon;
|
||||
public string Label { get; } = label;
|
||||
public bool IsSelected { get; set; }
|
||||
}
|
||||
@@ -47,7 +40,7 @@ public sealed partial class MainWindow
|
||||
InitializeComponent();
|
||||
|
||||
m_Level = BuildStarterLevel();
|
||||
m_EditorTools = Enum.GetValues<EEditorTool>().Select(tool => new EditorToolViewModel(tool, EditorToolIcon(tool), tool.ToString()) { IsSelected = tool == m_SelectedTool }).ToArray();
|
||||
m_EditorTools = Enum.GetValues<EEditorTool>().Select(tool => new EditorToolViewModel(tool, ToolLabel(tool)) { IsSelected = tool == m_SelectedTool }).ToArray();
|
||||
ToolPicker.ItemsSource = m_EditorTools;
|
||||
RefreshInspector();
|
||||
}
|
||||
@@ -63,18 +56,6 @@ public sealed partial class MainWindow
|
||||
m_RobotSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "robot.png");
|
||||
m_LeakSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "leak.png");
|
||||
m_HeatSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "heat.png");
|
||||
m_FireSprite = await LoadCanvasBitmapAsync(sender, "Images", "Props", "fire.png");
|
||||
|
||||
m_PropSprites[ECellProp.Reactor] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "reactor.png");
|
||||
m_PropSprites[ECellProp.CoolingPump] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "cooling-pump.png");
|
||||
m_PropSprites[ECellProp.Generator] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "generator.png");
|
||||
m_PropSprites[ECellProp.PressureRegulator] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "pressure-regulator.png");
|
||||
m_PropSprites[ECellProp.DiagnosticTerminal] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "diagnostic-terminal.png");
|
||||
m_PropSprites[ECellProp.ControlTerminal] = await LoadCanvasBitmapAsync(sender, "Images", "Props", "control-terminal.png");
|
||||
|
||||
m_PipeTilemaps[EPipeMedium.Pressure] = await LoadCanvasBitmapAsync(sender, "Images", "Pipes", "pipe-pressure-tilemap.png");
|
||||
m_PipeTilemaps[EPipeMedium.Coolant] = await LoadCanvasBitmapAsync(sender, "Images", "Pipes", "pipe-coolant-tilemap.png");
|
||||
m_PipeTilemaps[EPipeMedium.Fuel] = await LoadCanvasBitmapAsync(sender, "Images", "Pipes", "pipe-fuel-tilemap.png");
|
||||
}
|
||||
|
||||
private static async Task<CanvasBitmap> LoadCanvasBitmapAsync(CanvasControl sender, params string[] pathParts)
|
||||
@@ -85,13 +66,13 @@ public sealed partial class MainWindow
|
||||
|
||||
private void ToolToggle_Checked(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if ((sender as FrameworkElement)?.DataContext is EditorToolViewModel tool)
|
||||
{
|
||||
if ((sender as FrameworkElement)?.DataContext is not EditorToolViewModel tool)
|
||||
return;
|
||||
|
||||
m_SelectedTool = tool.Tool;
|
||||
foreach (var editorTool in m_EditorTools)
|
||||
editorTool.IsSelected = editorTool == tool;
|
||||
}
|
||||
}
|
||||
|
||||
private void New_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
@@ -115,8 +96,8 @@ public sealed partial class MainWindow
|
||||
return;
|
||||
|
||||
var json = await FileIO.ReadTextAsync(file);
|
||||
m_Level = LevelSerializer.Deserialize(json);
|
||||
m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
|
||||
var loaded = LevelSerializer.Deserialize(json);
|
||||
m_Level = loaded with { Forecasts = m_Simulation.Forecast(loaded) };
|
||||
m_CurrentFile = file;
|
||||
m_SelectedCell = null;
|
||||
RefreshInspector();
|
||||
@@ -133,6 +114,10 @@ public sealed partial class MainWindow
|
||||
{
|
||||
try
|
||||
{
|
||||
var report = new LevelValidator().Validate(m_Level);
|
||||
if (!report.IsValid)
|
||||
throw new InvalidOperationException(report.Errors[0].Message);
|
||||
|
||||
var file = m_CurrentFile;
|
||||
if (file is null)
|
||||
{
|
||||
@@ -156,9 +141,23 @@ public sealed partial class MainWindow
|
||||
}
|
||||
}
|
||||
|
||||
private void Simulate_Click(object sender, RoutedEventArgs e)
|
||||
private void EndTurn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
m_Level = m_Simulation.AdvanceTurn(m_Level);
|
||||
m_Level = m_Simulation.EndTurn(m_Level);
|
||||
RefreshInspector();
|
||||
LevelCanvas.Invalidate();
|
||||
}
|
||||
|
||||
private void Interact_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
m_Level = m_Simulation.InteractProp(m_Level);
|
||||
RefreshInspector();
|
||||
LevelCanvas.Invalidate();
|
||||
}
|
||||
|
||||
private void HeatShield_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
m_Level = m_Simulation.ApplyHeatShield(m_Level);
|
||||
RefreshInspector();
|
||||
LevelCanvas.Invalidate();
|
||||
}
|
||||
@@ -175,13 +174,14 @@ public sealed partial class MainWindow
|
||||
var point = e.GetCurrentPoint(LevelCanvas);
|
||||
if (point.Properties.IsRightButtonPressed)
|
||||
{
|
||||
RemovePropAt(point.Position);
|
||||
ClearAt(point.Position);
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (point.Properties.IsLeftButtonPressed)
|
||||
{
|
||||
if (!point.Properties.IsLeftButtonPressed)
|
||||
return;
|
||||
|
||||
_ = LevelCanvas.CapturePointer(e.Pointer);
|
||||
m_LeftPointerDown = true;
|
||||
m_LeftPointerDownPoint = point.Position;
|
||||
@@ -189,20 +189,20 @@ public sealed partial class MainWindow
|
||||
m_DragExceededClickThreshold = false;
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void LevelCanvas_PointerMoved(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
if (!m_LeftPointerDown)
|
||||
return;
|
||||
|
||||
var point = e.GetCurrentPoint(LevelCanvas);
|
||||
if (m_LeftPointerDown)
|
||||
{
|
||||
var deltaX = point.Position.X - m_LastPanPoint.X;
|
||||
var deltaY = point.Position.Y - m_LastPanPoint.Y;
|
||||
m_LastPanPoint = point.Position;
|
||||
|
||||
var totalDeltaX = point.Position.X - m_LeftPointerDownPoint.X;
|
||||
var totalDeltaY = point.Position.Y - m_LeftPointerDownPoint.Y;
|
||||
if (Math.Sqrt((totalDeltaX * totalDeltaX) + (totalDeltaY * totalDeltaY)) > c_ClickPixelThreshold)
|
||||
if (Math.Sqrt(totalDeltaX * totalDeltaX + totalDeltaY * totalDeltaY) > c_ClickPixelThreshold)
|
||||
m_DragExceededClickThreshold = true;
|
||||
|
||||
m_PanX += deltaX;
|
||||
@@ -210,8 +210,6 @@ public sealed partial class MainWindow
|
||||
ClampPan();
|
||||
LevelCanvas.Invalidate();
|
||||
e.Handled = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void LevelCanvas_PointerReleased(object sender, PointerRoutedEventArgs e)
|
||||
@@ -226,6 +224,12 @@ public sealed partial class MainWindow
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void LevelCanvas_PointerExited(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
m_LeftPointerDown = false;
|
||||
m_DragExceededClickThreshold = false;
|
||||
}
|
||||
|
||||
private void LevelCanvas_PointerWheelChanged(object sender, PointerRoutedEventArgs e)
|
||||
{
|
||||
var point = e.GetCurrentPoint(LevelCanvas);
|
||||
@@ -237,59 +241,34 @@ public sealed partial class MainWindow
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void ZoomAt(Point point, double zoomFactor)
|
||||
{
|
||||
var oldLayout = GetLayout();
|
||||
var cellX = (point.X - oldLayout.OriginX) / oldLayout.CellSize;
|
||||
var cellY = (point.Y - oldLayout.OriginY) / oldLayout.CellSize;
|
||||
|
||||
m_Zoom = Math.Clamp(m_Zoom * zoomFactor, c_MinZoom, c_MaxZoom);
|
||||
var newCellSize = GetBaseCellSize() * m_Zoom;
|
||||
var originWithoutPan = GetCenteredOrigin(newCellSize);
|
||||
m_PanX = point.X - originWithoutPan.X - (cellX * newCellSize);
|
||||
m_PanY = point.Y - originWithoutPan.Y - (cellY * newCellSize);
|
||||
ClampPan();
|
||||
LevelCanvas.Invalidate();
|
||||
}
|
||||
|
||||
private void SelectOrPaintAt(Point point)
|
||||
{
|
||||
if (m_SelectedTool == EEditorTool.Cursor)
|
||||
SelectAt(point);
|
||||
else
|
||||
PaintAt(point);
|
||||
}
|
||||
|
||||
private void SelectAt(Point point)
|
||||
{
|
||||
if (!TryGetGridPosition(point, out var position))
|
||||
return;
|
||||
|
||||
m_SelectedCell = position;
|
||||
RefreshInspector();
|
||||
LevelCanvas.Invalidate();
|
||||
}
|
||||
|
||||
private void RemovePropAt(Point point)
|
||||
if (m_SelectedTool != EEditorTool.Cursor)
|
||||
{
|
||||
if (!TryGetGridPosition(point, out var position))
|
||||
return;
|
||||
|
||||
var cell = m_Level.GetCell(position);
|
||||
m_SelectedCell = position;
|
||||
m_Level = m_Level.SetCell(position, cell with { Prop = ECellProp.None });
|
||||
m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
|
||||
RefreshInspector();
|
||||
LevelCanvas.Invalidate();
|
||||
}
|
||||
|
||||
private void PaintAt(Point point)
|
||||
{
|
||||
if (!TryGetGridPosition(point, out var position))
|
||||
return;
|
||||
|
||||
m_SelectedCell = position;
|
||||
m_Level = LevelEditor.Apply(m_Level, position, m_SelectedTool);
|
||||
m_Level = AutoBindReactors(m_Level);
|
||||
m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
|
||||
}
|
||||
|
||||
RefreshInspector();
|
||||
LevelCanvas.Invalidate();
|
||||
}
|
||||
|
||||
private void ClearAt(Point point)
|
||||
{
|
||||
if (!TryGetGridPosition(point, out var position))
|
||||
return;
|
||||
|
||||
m_SelectedCell = position;
|
||||
m_Level = m_Level.SetProp(position, new()).SetSurface(position, new()) with {
|
||||
Leaks = m_Level.Leaks.Where(leak => leak.AccessPosition != position && leak.UndergroundPosition != position).ToArray(),
|
||||
Doors = m_Level.Doors.Where(door => door.A != position && door.B != position).ToArray(),
|
||||
Reactors = m_Level.Reactors.Where(reactor => reactor.ControlPosition != position).ToArray()
|
||||
};
|
||||
m_Level = m_Level with { Forecasts = m_Simulation.Forecast(m_Level) };
|
||||
RefreshInspector();
|
||||
LevelCanvas.Invalidate();
|
||||
@@ -302,212 +281,125 @@ public sealed partial class MainWindow
|
||||
|
||||
drawing.Clear(ColorHelper.FromArgb(255, 16, 18, 21));
|
||||
DrawTerrain(drawing, layout);
|
||||
DrawCellOverlays(drawing, layout);
|
||||
//DrawGrid(drawing, layout);
|
||||
DrawUnderground(drawing, layout);
|
||||
DrawSurface(drawing, layout);
|
||||
DrawDoors(drawing, layout);
|
||||
DrawProps(drawing, layout);
|
||||
DrawLeaks(drawing, layout);
|
||||
DrawRobot(drawing, layout);
|
||||
DrawGrid(drawing, layout);
|
||||
}
|
||||
|
||||
private void DrawTerrain(CanvasDrawingSession drawing, CanvasLayout layout)
|
||||
{
|
||||
for (var y = 0; y <= m_Level.Height; y++)
|
||||
foreach (var position in AllPositions())
|
||||
{
|
||||
for (var x = 0; x <= m_Level.Width; x++)
|
||||
{
|
||||
DrawDualTerrainTile(drawing, layout.DualTileRect(x, y), GetDualTileMask(x, y));
|
||||
}
|
||||
var rect = layout.CellRect(position);
|
||||
var color = m_Level.GetTerrain(position) == ECellTerrain.Wall ? ColorHelper.FromArgb(255, 41, 47, 52) : ColorHelper.FromArgb(255, 32, 38, 42);
|
||||
drawing.FillRectangle(rect, color);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawCellOverlays(CanvasDrawingSession drawing, CanvasLayout layout)
|
||||
private void DrawUnderground(CanvasDrawingSession drawing, CanvasLayout layout)
|
||||
{
|
||||
for (var y = 0; y < m_Level.Height; y++)
|
||||
foreach (var position in AllPositions())
|
||||
{
|
||||
for (var x = 0; x < m_Level.Width; x++)
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
var cell = m_Level.GetCell(position);
|
||||
var rect = layout.CellRect(x, y);
|
||||
|
||||
DrawPipe(drawing, position, cell, rect);
|
||||
|
||||
if (cell.LeakRate > 0)
|
||||
DrawImage(drawing, m_LeakSprite, Inset(rect, 0.12));
|
||||
|
||||
if (cell.Hazards.Heat > 0)
|
||||
DrawImage(drawing, m_HeatSprite, Inset(rect, 0.08), Math.Clamp(cell.Hazards.Heat / 10.0f, 0.35f, 0.9f));
|
||||
|
||||
if (cell.Hazards.Fire)
|
||||
DrawImage(drawing, m_FireSprite, Inset(rect, 0.08));
|
||||
|
||||
if (m_SelectedCell == position)
|
||||
drawing.DrawRectangle(rect, Colors.White, 3);
|
||||
|
||||
DrawCellProp(drawing, cell, rect);
|
||||
}
|
||||
var rect = Inset(layout.CellRect(position), 0.18);
|
||||
DrawUndergroundCell(drawing, rect, position, ECarrierType.Fuel, c_FuelColor);
|
||||
DrawUndergroundCell(drawing, Inset(rect, -0.08), position, ECarrierType.Coolant, c_CoolantColor);
|
||||
DrawUndergroundCell(drawing, Inset(rect, -0.16), position, ECarrierType.Electricity, c_ElectricityColor);
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawPipe(CanvasDrawingSession drawing, GridPosition position, CellState cell, Rect rect)
|
||||
private void DrawUndergroundCell(CanvasDrawingSession drawing, Rect rect, GridPosition position, ECarrierType carrier, Color color)
|
||||
{
|
||||
if (!cell.HasPipe || !m_PipeTilemaps.TryGetValue(cell.Pipe, out var tilemap))
|
||||
var cell = m_Level.GetUnderground(position, carrier);
|
||||
if (!cell.IsPresent)
|
||||
return;
|
||||
|
||||
var sourceRect = PipeTileSourceRect(GetPipeConnectionMask(position, cell.Pipe));
|
||||
drawing.DrawImage(tilemap, rect, sourceRect, 1.0f, CanvasImageInterpolation.HighQualityCubic);
|
||||
drawing.DrawRectangle(rect, color, cell.State == EUndergroundState.Leaking ? 4 : 2);
|
||||
if (cell.Amount > 0 || cell.Intensity > 0)
|
||||
drawing.FillCircle((float)(rect.X + rect.Width / 2), (float)(rect.Y + rect.Height / 2), (float)Math.Max(2, rect.Width * 0.08), color);
|
||||
}
|
||||
|
||||
private int GetPipeConnectionMask(GridPosition position, EPipeMedium medium)
|
||||
private void DrawSurface(CanvasDrawingSession drawing, CanvasLayout layout)
|
||||
{
|
||||
var mask = 0;
|
||||
if (HasMatchingPipe(position with { Y = position.Y - 1 }, medium))
|
||||
mask |= c_NorthConnection;
|
||||
|
||||
if (HasMatchingPipe(position with { X = position.X + 1 }, medium))
|
||||
mask |= c_EastConnection;
|
||||
|
||||
if (HasMatchingPipe(position with { Y = position.Y + 1 }, medium))
|
||||
mask |= c_SouthConnection;
|
||||
|
||||
if (HasMatchingPipe(position with { X = position.X - 1 }, medium))
|
||||
mask |= c_WestConnection;
|
||||
|
||||
return mask;
|
||||
foreach (var position in AllPositions().Where(m_Level.IsFloor))
|
||||
{
|
||||
var surface = m_Level.GetSurface(position);
|
||||
var rect = layout.CellRect(position);
|
||||
FillHazard(drawing, rect, surface.Fuel, c_FuelColor, 0.08);
|
||||
FillHazard(drawing, rect, surface.Coolant, c_CoolantColor, 0.18);
|
||||
FillHazard(drawing, rect, surface.Electricity, c_ElectricityColor, 0.28);
|
||||
if (surface.Heat > 0)
|
||||
DrawImage(drawing, m_HeatSprite, Inset(rect, 0.18), Math.Clamp(surface.Heat / Balancing.Current.MaxValue, 0.25f, 0.9f));
|
||||
}
|
||||
}
|
||||
|
||||
private bool HasMatchingPipe(GridPosition position, EPipeMedium medium)
|
||||
private static void FillHazard(CanvasDrawingSession drawing, Rect rect, float amount, Color color, double inset)
|
||||
{
|
||||
return m_Level.InBounds(position) && m_Level.GetCell(position).Pipe == medium;
|
||||
}
|
||||
|
||||
private static Rect PipeTileSourceRect(int connectionMask)
|
||||
{
|
||||
var tileIndex = connectionMask switch {
|
||||
0 => 0,
|
||||
c_NorthConnection => 1,
|
||||
c_EastConnection => 2,
|
||||
c_SouthConnection => 3,
|
||||
c_WestConnection => 4,
|
||||
c_NorthConnection | c_EastConnection => 5,
|
||||
c_EastConnection | c_SouthConnection => 6,
|
||||
c_SouthConnection | c_WestConnection => 7,
|
||||
c_WestConnection | c_NorthConnection => 8,
|
||||
c_NorthConnection | c_SouthConnection => 9,
|
||||
c_EastConnection | c_WestConnection => 10,
|
||||
c_NorthConnection | c_EastConnection | c_SouthConnection => 11,
|
||||
c_EastConnection | c_SouthConnection | c_WestConnection => 12,
|
||||
c_SouthConnection | c_WestConnection | c_NorthConnection => 13,
|
||||
c_WestConnection | c_NorthConnection | c_EastConnection => 14,
|
||||
c_NorthConnection | c_EastConnection | c_SouthConnection | c_WestConnection => 15,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(connectionMask), connectionMask, "Unsupported pipe connection mask.")
|
||||
};
|
||||
|
||||
return new(
|
||||
tileIndex % c_PipeTilemapColumns * c_PipeTilemapTileSize,
|
||||
tileIndex / c_PipeTilemapColumns * c_PipeTilemapTileSize,
|
||||
c_PipeTilemapTileSize,
|
||||
c_PipeTilemapTileSize);
|
||||
}
|
||||
|
||||
private static void DrawImage(CanvasDrawingSession drawing, CanvasBitmap? image, Rect rect, float opacity = 1)
|
||||
{
|
||||
if (image is not null)
|
||||
drawing.DrawImage(image, rect, image.Bounds, opacity, CanvasImageInterpolation.HighQualityCubic);
|
||||
}
|
||||
|
||||
private static Rect Inset(Rect rect, double fraction)
|
||||
{
|
||||
var inset = rect.Width * fraction;
|
||||
return new(rect.X + inset, rect.Y + inset, rect.Width - inset * 2, rect.Height - inset * 2);
|
||||
}
|
||||
|
||||
private void DrawDualTerrainTile(CanvasDrawingSession drawing, Rect rect, int floorMask)
|
||||
{
|
||||
if (m_TerrainTilemap is null)
|
||||
if (amount <= 0)
|
||||
return;
|
||||
|
||||
var wallMask = c_AllCorners ^ floorMask;
|
||||
var sourceRect = TilemapSourceRect(wallMask);
|
||||
drawing.DrawImage(m_TerrainTilemap, rect, sourceRect, 1.0f, CanvasImageInterpolation.HighQualityCubic);
|
||||
var alpha = (byte)Math.Clamp(40 + amount / Balancing.Current.MaxValue * 130, 40, 170);
|
||||
drawing.FillRectangle(Inset(rect, inset), ColorHelper.FromArgb(alpha, color.R, color.G, color.B));
|
||||
}
|
||||
|
||||
private static Rect TilemapSourceRect(int wallMask)
|
||||
private void DrawDoors(CanvasDrawingSession drawing, CanvasLayout layout)
|
||||
{
|
||||
var tilePosition = wallMask switch {
|
||||
c_BottomLeftCorner => new(0, 0),
|
||||
c_TopRightCorner | c_BottomRightCorner => new(1, 0),
|
||||
c_TopLeftCorner | c_BottomLeftCorner | c_BottomRightCorner => new(2, 0),
|
||||
c_BottomLeftCorner | c_BottomRightCorner => new(3, 0),
|
||||
c_TopLeftCorner | c_BottomRightCorner => new(0, 1),
|
||||
c_BottomLeftCorner | c_TopRightCorner | c_BottomRightCorner => new(1, 1),
|
||||
c_AllCorners => new(2, 1),
|
||||
c_TopLeftCorner | c_BottomLeftCorner | c_TopRightCorner => new(3, 1),
|
||||
c_TopRightCorner => new(0, 2),
|
||||
c_TopLeftCorner | c_TopRightCorner => new(1, 2),
|
||||
c_TopLeftCorner | c_TopRightCorner | c_BottomRightCorner => new(2, 2),
|
||||
c_BottomLeftCorner | c_TopLeftCorner => new(3, 2),
|
||||
0 => new(0, 3),
|
||||
c_BottomRightCorner => new(1, 3),
|
||||
c_BottomLeftCorner | c_TopRightCorner => new(2, 3),
|
||||
c_TopLeftCorner => new GridPosition(3, 3),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(wallMask), wallMask, "Unsupported tile mask.")
|
||||
};
|
||||
|
||||
return new(
|
||||
tilePosition.X * c_TilemapTileSize,
|
||||
tilePosition.Y * c_TilemapTileSize,
|
||||
c_TilemapTileSize,
|
||||
c_TilemapTileSize);
|
||||
foreach (var door in m_Level.Doors)
|
||||
{
|
||||
var centerA = Center(layout.CellRect(door.A));
|
||||
var centerB = Center(layout.CellRect(door.B));
|
||||
drawing.DrawLine((float)centerA.X, (float)centerA.Y, (float)centerB.X, (float)centerB.Y, door.State == EDoorState.Open ? Colors.LightGreen : Colors.OrangeRed, 5);
|
||||
}
|
||||
}
|
||||
|
||||
private int GetDualTileMask(int x, int y)
|
||||
private void DrawProps(CanvasDrawingSession drawing, CanvasLayout layout)
|
||||
{
|
||||
var mask = 0;
|
||||
if (GetTerrainOrWall(x - 1, y - 1) == ECellTerrain.Floor)
|
||||
mask |= c_TopLeftCorner;
|
||||
foreach (var position in AllPositions())
|
||||
{
|
||||
var prop = m_Level.GetProp(position);
|
||||
if (prop.Type == EPropType.None)
|
||||
continue;
|
||||
|
||||
if (GetTerrainOrWall(x, y - 1) == ECellTerrain.Floor)
|
||||
mask |= c_TopRightCorner;
|
||||
|
||||
if (GetTerrainOrWall(x - 1, y) == ECellTerrain.Floor)
|
||||
mask |= c_BottomLeftCorner;
|
||||
|
||||
if (GetTerrainOrWall(x, y) == ECellTerrain.Floor)
|
||||
mask |= c_BottomRightCorner;
|
||||
|
||||
return mask;
|
||||
var rect = Inset(layout.CellRect(position), 0.18);
|
||||
drawing.FillRoundedRectangle(rect, 4, 4, PropColor(prop));
|
||||
DrawCenteredText(drawing, PropLabel(prop), rect, Colors.White, Math.Max(10, (float)(layout.CellSize * 0.22)));
|
||||
}
|
||||
}
|
||||
|
||||
private ECellTerrain GetTerrainOrWall(int x, int y)
|
||||
private void DrawLeaks(CanvasDrawingSession drawing, CanvasLayout layout)
|
||||
{
|
||||
var position = new GridPosition(x, y);
|
||||
return m_Level.InBounds(position) ? m_Level.GetCell(position).Terrain : ECellTerrain.Wall;
|
||||
}
|
||||
|
||||
private void DrawCellProp(CanvasDrawingSession drawing, CellState cell, Rect rect)
|
||||
{
|
||||
if (m_PropSprites.TryGetValue(cell.Prop, out var sprite))
|
||||
drawing.DrawImage(sprite, rect, sprite.Bounds, 1.0f, CanvasImageInterpolation.HighQualityCubic);
|
||||
}
|
||||
|
||||
private void DrawGrid(CanvasDrawingSession drawing, CanvasLayout layout)
|
||||
{
|
||||
for (var x = 0; x <= m_Level.Width; x++)
|
||||
{
|
||||
var xPos = (float)(layout.OriginX + (x * layout.CellSize));
|
||||
drawing.DrawLine(xPos, (float)layout.OriginY, xPos, (float)(layout.OriginY + (m_Level.Height * layout.CellSize)), ColorHelper.FromArgb(120, 91, 104, 115), 1);
|
||||
}
|
||||
|
||||
for (var y = 0; y <= m_Level.Height; y++)
|
||||
{
|
||||
var yPos = (float)(layout.OriginY + (y * layout.CellSize));
|
||||
drawing.DrawLine((float)layout.OriginX, yPos, (float)(layout.OriginX + (m_Level.Width * layout.CellSize)), yPos, ColorHelper.FromArgb(120, 91, 104, 115), 1);
|
||||
}
|
||||
foreach (var leak in m_Level.Leaks.Where(leak => !leak.Repaired))
|
||||
DrawImage(drawing, m_LeakSprite, Inset(layout.CellRect(leak.AccessPosition), 0.1));
|
||||
}
|
||||
|
||||
private void DrawRobot(CanvasDrawingSession drawing, CanvasLayout layout)
|
||||
{
|
||||
var rect = layout.CellRect(m_Level.Robot.X, m_Level.Robot.Y);
|
||||
DrawImage(drawing, m_RobotSprite, rect);
|
||||
DrawImage(drawing, m_RobotSprite, Inset(layout.CellRect(m_Level.Robot.Position), 0.04));
|
||||
}
|
||||
|
||||
private void DrawGrid(CanvasDrawingSession drawing, CanvasLayout layout)
|
||||
{
|
||||
foreach (var position in AllPositions())
|
||||
{
|
||||
var rect = layout.CellRect(position);
|
||||
drawing.DrawRectangle(rect, ColorHelper.FromArgb(90, 91, 104, 115), 1);
|
||||
if (m_SelectedCell == position)
|
||||
drawing.DrawRectangle(rect, Colors.White, 3);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawCenteredText(CanvasDrawingSession drawing, string text, Rect rect, Color color, float fontSize)
|
||||
{
|
||||
using var format = new CanvasTextFormat {
|
||||
FontSize = fontSize,
|
||||
HorizontalAlignment = CanvasHorizontalAlignment.Center,
|
||||
VerticalAlignment = CanvasVerticalAlignment.Center,
|
||||
WordWrapping = CanvasWordWrapping.NoWrap
|
||||
};
|
||||
drawing.DrawText(text, rect, color, format);
|
||||
}
|
||||
|
||||
private bool TryGetGridPosition(Point point, out GridPosition position)
|
||||
@@ -539,19 +431,14 @@ public sealed partial class MainWindow
|
||||
{
|
||||
var availableWidth = Math.Max(1, LevelCanvas.ActualWidth);
|
||||
var availableHeight = Math.Max(1, LevelCanvas.ActualHeight);
|
||||
return new((availableWidth - (cellSize * m_Level.Width)) / 2, (availableHeight - (cellSize * m_Level.Height)) / 2);
|
||||
return new((availableWidth - cellSize * m_Level.Width) / 2, (availableHeight - cellSize * m_Level.Height) / 2);
|
||||
}
|
||||
|
||||
private void ClampPan()
|
||||
{
|
||||
var cellSize = GetBaseCellSize() * m_Zoom;
|
||||
var contentWidth = cellSize * m_Level.Width;
|
||||
var contentHeight = cellSize * m_Level.Height;
|
||||
var availableWidth = Math.Max(1, LevelCanvas.ActualWidth);
|
||||
var availableHeight = Math.Max(1, LevelCanvas.ActualHeight);
|
||||
|
||||
m_PanX = ClampAxisPan(m_PanX, contentWidth, availableWidth);
|
||||
m_PanY = ClampAxisPan(m_PanY, contentHeight, availableHeight);
|
||||
m_PanX = ClampAxisPan(m_PanX, cellSize * m_Level.Width, Math.Max(1, LevelCanvas.ActualWidth));
|
||||
m_PanY = ClampAxisPan(m_PanY, cellSize * m_Level.Height, Math.Max(1, LevelCanvas.ActualHeight));
|
||||
}
|
||||
|
||||
private static double ClampAxisPan(double pan, double contentSize, double availableSize)
|
||||
@@ -563,152 +450,258 @@ public sealed partial class MainWindow
|
||||
return Math.Clamp(pan, -maxPan, maxPan);
|
||||
}
|
||||
|
||||
private void ZoomAt(Point point, double zoomFactor)
|
||||
{
|
||||
var oldLayout = GetLayout();
|
||||
var cellX = (point.X - oldLayout.OriginX) / oldLayout.CellSize;
|
||||
var cellY = (point.Y - oldLayout.OriginY) / oldLayout.CellSize;
|
||||
|
||||
m_Zoom = Math.Clamp(m_Zoom * zoomFactor, c_MinZoom, c_MaxZoom);
|
||||
var newCellSize = GetBaseCellSize() * m_Zoom;
|
||||
var originWithoutPan = GetCenteredOrigin(newCellSize);
|
||||
m_PanX = point.X - originWithoutPan.X - cellX * newCellSize;
|
||||
m_PanY = point.Y - originWithoutPan.Y - cellY * newCellSize;
|
||||
ClampPan();
|
||||
LevelCanvas.Invalidate();
|
||||
}
|
||||
|
||||
private void RefreshInspector()
|
||||
{
|
||||
LevelNameText.Text = m_Level.Name;
|
||||
TurnText.Text = m_Level.Global.Turn.ToString(CultureInfo.InvariantCulture);
|
||||
StatusText.Text = m_Level.Global.Status;
|
||||
GlobalText.Text = $"Power: {m_Level.Global.Power}/10\n" + $"Cooling: {m_Level.Global.Cooling}/10\n" + $"Core Heat: {m_Level.Global.CoreHeat}/10\n" + $"Facility Stability: {m_Level.Global.FacilityStability}/10";
|
||||
StatusText.Text = $"{m_Level.Global.LevelState}: {m_Level.Global.Status}";
|
||||
GlobalText.Text = $"Actions: {m_Level.Global.ActionsRemaining}/{Balancing.Current.ActionsPerTurn}\n"
|
||||
+ $"All-seeing-eye: {(m_Level.Global.AllSeeingEyeUnlocked ? "unlocked" : "locked")}\n"
|
||||
+ $"Inventory: F {m_Level.Robot.FuelNeutralizers}, C {m_Level.Robot.CoolantNeutralizers}, E {m_Level.Robot.ElectricityNeutralizers}, H {m_Level.Robot.HeatShields}\n"
|
||||
+ $"Heat immunity steps: {m_Level.Robot.HeatImmunitySteps}\n"
|
||||
+ $"Reactors: {m_Level.Reactors.Count}, Leaks: {m_Level.Leaks.Count}, Doors: {m_Level.Doors.Count}";
|
||||
|
||||
if (m_SelectedCell is { } position && m_Level.InBounds(position))
|
||||
{
|
||||
var cell = m_Level.GetCell(position);
|
||||
CellText.Text = $"Position: {position.X},{position.Y}\n" + $"Terrain: {cell.Terrain}\n" + $"Prop: {cell.Prop}\n" + $"Pipe: {cell.Pipe}\n" + $"Flow: {cell.Flow}, Pressure: {cell.Pressure}\n" + $"Integrity: {cell.Integrity}, Leak: {cell.LeakRate}\n" + $"Heat: {cell.Hazards.Heat}, Smoke: {cell.Hazards.Smoke}\n" + $"Fuel Vapor: {cell.Hazards.FuelVapor}, Fuel: {cell.Hazards.LiquidFuel}\n" + $"Coolant: {cell.Hazards.CoolantPooling}, Charge: {cell.Hazards.ElectricalCharge}";
|
||||
}
|
||||
else
|
||||
CellText.Text = "No cell selected.";
|
||||
|
||||
ForecastList.ItemsSource = m_Level.Forecasts.Select(forecast => new ForecastViewModel(FailureIcon(forecast.Kind), forecast.Message)).ToArray();
|
||||
CellText.Text = m_SelectedCell is { } position && m_Level.InBounds(position) ? CellInspectionText(position) : "No cell selected.";
|
||||
ForecastList.ItemsSource = m_Level.Forecasts.Select(forecast => new ForecastViewModel($"{forecast.Turns}: {forecast.Message}")).ToArray();
|
||||
}
|
||||
|
||||
private static BitmapImage FailureIcon(EFailureKind kind)
|
||||
private string CellInspectionText(GridPosition position)
|
||||
{
|
||||
return ImageFromOutputPath("Images", "Failures", FailureIconFileName(kind));
|
||||
var prop = m_Level.GetProp(position);
|
||||
var surface = m_Level.GetSurface(position);
|
||||
var fuel = m_Level.GetUnderground(position, ECarrierType.Fuel);
|
||||
var coolant = m_Level.GetUnderground(position, ECarrierType.Coolant);
|
||||
var electricity = m_Level.GetUnderground(position, ECarrierType.Electricity);
|
||||
return $"Position: {position.X},{position.Y}\n"
|
||||
+ $"Terrain: {m_Level.GetTerrain(position)}\n"
|
||||
+ $"Prop: {prop.Type} {prop.Carrier} {prop.SwitchState} {prop.ServiceState}\n"
|
||||
+ $"Fuel: {UndergroundText(fuel)}\n"
|
||||
+ $"Coolant: {UndergroundText(coolant)}\n"
|
||||
+ $"Electricity: {UndergroundText(electricity)}\n"
|
||||
+ $"Surface fuel/coolant/electricity/heat: {Format(surface.Fuel)} / {Format(surface.Coolant)} / {Format(surface.Electricity)} / {Format(surface.Heat)}\n"
|
||||
+ $"Blocks F/C/E: {surface.FuelBlockTurns}/{surface.CoolantBlockTurns}/{surface.ElectricityBlockTurns}";
|
||||
}
|
||||
|
||||
private static string FailureIconFileName(EFailureKind kind)
|
||||
private static string UndergroundText(UndergroundCell cell)
|
||||
{
|
||||
return kind switch {
|
||||
EFailureKind.PipeBurst => "failure-pipe-burst.png",
|
||||
EFailureKind.Ignition => "failure-ignition.png",
|
||||
EFailureKind.Meltdown => "failure-meltdown.png",
|
||||
EFailureKind.StabilityCollapse => "failure-stability-collapse.png",
|
||||
EFailureKind.ReactorReady => "failure-reactor-ready.png",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, "Unsupported failure kind.")
|
||||
};
|
||||
return $"{cell.State} amount {Format(cell.Amount)} intensity {Format(cell.Intensity)}";
|
||||
}
|
||||
|
||||
private static BitmapImage? EditorToolIcon(EEditorTool tool)
|
||||
private static string Format(float value)
|
||||
{
|
||||
return tool switch {
|
||||
EEditorTool.Cursor => PropImage("cursor.png"),
|
||||
EEditorTool.Floor => PropImage("floor.png"),
|
||||
EEditorTool.Wall => PropImage("wall.png"),
|
||||
EEditorTool.Reactor => PropImage("reactor.png"),
|
||||
EEditorTool.CoolingPump => PropImage("cooling-pump.png"),
|
||||
EEditorTool.Generator => PropImage("generator.png"),
|
||||
EEditorTool.PressureRegulator => PropImage("pressure-regulator.png"),
|
||||
EEditorTool.DiagnosticTerminal => PropImage("diagnostic-terminal.png"),
|
||||
EEditorTool.ControlTerminal => PropImage("control-terminal.png"),
|
||||
EEditorTool.CoolantPipe => PipeImage("pipe-coolant-tilemap.png"),
|
||||
EEditorTool.FuelPipe => PipeImage("pipe-fuel-tilemap.png"),
|
||||
EEditorTool.PressurePipe => PipeImage("pipe-pressure-tilemap.png"),
|
||||
EEditorTool.Leak => PropImage("leak.png"),
|
||||
EEditorTool.Repair => PropImage("repair.png"),
|
||||
EEditorTool.Heat => PropImage("heat.png"),
|
||||
EEditorTool.Fire => PropImage("fire.png"),
|
||||
EEditorTool.Robot => PropImage("robot.png"),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(tool), tool, "Unsupported editor tool.")
|
||||
};
|
||||
}
|
||||
|
||||
private static BitmapImage PropImage(string fileName)
|
||||
{
|
||||
return ImageFromOutputPath("Images", "Props", fileName);
|
||||
}
|
||||
|
||||
private static BitmapImage PipeImage(string fileName)
|
||||
{
|
||||
return ImageFromOutputPath("Images", "Pipes", fileName);
|
||||
}
|
||||
|
||||
private static BitmapImage ImageFromOutputPath(params string[] pathParts)
|
||||
{
|
||||
return new(new(Path.Combine([AppContext.BaseDirectory, .. pathParts])));
|
||||
return value.ToString("0.0", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
private static LevelState BuildStarterLevel()
|
||||
{
|
||||
var level = LevelState.Create("Cooling Sector B", 16, 12);
|
||||
level = level.SetCell(new(3, 5), new() {
|
||||
Prop = ECellProp.CoolingPump,
|
||||
Pipe = EPipeMedium.Coolant,
|
||||
Flow = 5,
|
||||
Pressure = 5,
|
||||
Powered = true
|
||||
});
|
||||
level = level.SetCell(new(4, 5), new() {
|
||||
Pipe = EPipeMedium.Coolant,
|
||||
Flow = 5,
|
||||
Pressure = 7
|
||||
});
|
||||
level = level.SetCell(new(5, 5), new() {
|
||||
Pipe = EPipeMedium.Coolant,
|
||||
Flow = 3,
|
||||
Pressure = 8,
|
||||
LeakRate = 2,
|
||||
Integrity = 4
|
||||
});
|
||||
level = level.SetCell(new(6, 5), new() {
|
||||
Pipe = EPipeMedium.Coolant,
|
||||
Flow = 3,
|
||||
Pressure = 7
|
||||
});
|
||||
level = level.SetCell(new(8, 5), new() {
|
||||
Prop = ECellProp.Reactor,
|
||||
Hazards = new() {
|
||||
Heat = 6,
|
||||
Stability = 8
|
||||
level = AddNetwork(level, ECarrierType.Fuel, new(2, 3), new(5, 3));
|
||||
level = AddNetwork(level, ECarrierType.Coolant, new(2, 5), new(5, 5));
|
||||
level = AddNetwork(level, ECarrierType.Electricity, new(2, 7), new(5, 7));
|
||||
level = level.SetProp(new(2, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
||||
level = level.SetProp(new(2, 5), new() { Type = EPropType.Flow, Carrier = ECarrierType.Coolant });
|
||||
level = level.SetProp(new(2, 7), new() { Type = EPropType.Flow, Carrier = ECarrierType.Electricity });
|
||||
level = level.SetProp(new(5, 3), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Fuel });
|
||||
level = level.SetProp(new(5, 5), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Coolant });
|
||||
level = level.SetProp(new(5, 7), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Electricity });
|
||||
level = level.SetProp(new(10, 5), new() { Type = EPropType.ReactorControl, ReactorId = 1 });
|
||||
level = level.SetProp(new(11, 4), new() { Type = EPropType.AllSeeingEyeTerminal });
|
||||
level = level.SetProp(new(11, 6), new() { Type = EPropType.RemedySupply, RemedyType = ERemedyType.HeatShield });
|
||||
level = level.SetUnderground(new(4, 5), ECarrierType.Coolant, new() { State = EUndergroundState.Leaking }) with {
|
||||
Leaks = [new LeakState { Carrier = ECarrierType.Coolant, UndergroundPosition = new(4, 5), AccessPosition = new(4, 5) }],
|
||||
Doors = [new DoorState { A = new(8, 5), B = new(9, 5), State = EDoorState.Closed }],
|
||||
Robot = new() { Position = new(10, 5) },
|
||||
Reactors = [
|
||||
new ReactorBinding {
|
||||
ReactorId = 1,
|
||||
ControlPosition = new(10, 5),
|
||||
FuelConsumerPosition = new(5, 3),
|
||||
CoolantConsumerPosition = new(5, 5),
|
||||
ElectricityConsumerPosition = new(5, 7)
|
||||
}
|
||||
});
|
||||
level = level.SetCell(new(2, 8), new() {
|
||||
Prop = ECellProp.Generator,
|
||||
Pipe = EPipeMedium.Fuel,
|
||||
Flow = 4,
|
||||
Pressure = 6,
|
||||
Powered = true
|
||||
});
|
||||
level = level.SetCell(new(11, 4), new() {
|
||||
Prop = ECellProp.DiagnosticTerminal,
|
||||
Powered = true
|
||||
});
|
||||
level = level.SetCell(new(12, 8), new() {
|
||||
Prop = ECellProp.ControlTerminal,
|
||||
Powered = true
|
||||
});
|
||||
]
|
||||
};
|
||||
|
||||
return level with { Forecasts = new SimulationEngine().Forecast(level) };
|
||||
}
|
||||
|
||||
private const int c_TilemapTileSize = 512;
|
||||
private const int c_PipeTilemapTileSize = 256;
|
||||
private const int c_PipeTilemapColumns = 4;
|
||||
private const int c_TopLeftCorner = 1;
|
||||
private const int c_TopRightCorner = 2;
|
||||
private const int c_BottomLeftCorner = 4;
|
||||
private const int c_BottomRightCorner = 8;
|
||||
private const int c_AllCorners = c_TopLeftCorner | c_TopRightCorner | c_BottomLeftCorner | c_BottomRightCorner;
|
||||
private const int c_NorthConnection = 1;
|
||||
private const int c_EastConnection = 2;
|
||||
private const int c_SouthConnection = 4;
|
||||
private const int c_WestConnection = 8;
|
||||
private static LevelState AddNetwork(LevelState level, ECarrierType carrier, GridPosition start, GridPosition end)
|
||||
{
|
||||
var minX = Math.Min(start.X, end.X);
|
||||
var maxX = Math.Max(start.X, end.X);
|
||||
var minY = Math.Min(start.Y, end.Y);
|
||||
var maxY = Math.Max(start.Y, end.Y);
|
||||
for (var y = minY; y <= maxY; y++)
|
||||
{
|
||||
for (var x = minX; x <= maxX; x++)
|
||||
level = level.SetUnderground(new(x, y), carrier, new() { State = EUndergroundState.Intact });
|
||||
}
|
||||
|
||||
return level;
|
||||
}
|
||||
|
||||
private static LevelState AutoBindReactors(LevelState level)
|
||||
{
|
||||
if (level.Reactors.Count == 0)
|
||||
return level;
|
||||
|
||||
var fuel = FirstConsumer(level, ECarrierType.Fuel);
|
||||
var coolant = FirstConsumer(level, ECarrierType.Coolant);
|
||||
var electricity = FirstConsumer(level, ECarrierType.Electricity);
|
||||
var reactors = level.Reactors.Select(reactor => reactor with {
|
||||
FuelConsumerPosition = fuel ?? reactor.FuelConsumerPosition,
|
||||
CoolantConsumerPosition = coolant ?? reactor.CoolantConsumerPosition,
|
||||
ElectricityConsumerPosition = electricity ?? reactor.ElectricityConsumerPosition
|
||||
})
|
||||
.ToArray();
|
||||
return level with { Reactors = reactors };
|
||||
}
|
||||
|
||||
private static GridPosition? FirstConsumer(LevelState level, ECarrierType carrier)
|
||||
{
|
||||
return AllPositions(level).FirstOrDefault(position => level.GetProp(position) is { Type: EPropType.Consumer, Carrier: var propCarrier } && propCarrier == carrier);
|
||||
}
|
||||
|
||||
private IEnumerable<GridPosition> AllPositions()
|
||||
{
|
||||
return AllPositions(m_Level);
|
||||
}
|
||||
|
||||
private static IEnumerable<GridPosition> AllPositions(LevelState level)
|
||||
{
|
||||
for (var y = 0; y < level.Height; y++)
|
||||
{
|
||||
for (var x = 0; x < level.Width; x++)
|
||||
yield return new(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
private static Rect Inset(Rect rect, double fraction)
|
||||
{
|
||||
var inset = rect.Width * fraction;
|
||||
return new(rect.X + inset, rect.Y + inset, rect.Width - inset * 2, rect.Height - inset * 2);
|
||||
}
|
||||
|
||||
private static Point Center(Rect rect)
|
||||
{
|
||||
return new(rect.X + rect.Width / 2, rect.Y + rect.Height / 2);
|
||||
}
|
||||
|
||||
private static void DrawImage(CanvasDrawingSession drawing, CanvasBitmap? image, Rect rect, float opacity = 1)
|
||||
{
|
||||
if (image is not null)
|
||||
drawing.DrawImage(image, rect, image.Bounds, opacity);
|
||||
}
|
||||
|
||||
private static Color PropColor(PropState prop)
|
||||
{
|
||||
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.Door => ColorHelper.FromArgb(255, 187, 119, 55),
|
||||
EPropType.AllSeeingEyeTerminal => ColorHelper.FromArgb(255, 85, 151, 156),
|
||||
EPropType.RemedySupply => ColorHelper.FromArgb(255, 76, 145, 86),
|
||||
EPropType.ReactorControl => ColorHelper.FromArgb(255, 177, 72, 73),
|
||||
_ => Colors.Gray
|
||||
};
|
||||
}
|
||||
|
||||
private static Color CarrierColor(ECarrierType carrier)
|
||||
{
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => c_FuelColor,
|
||||
ECarrierType.Coolant => c_CoolantColor,
|
||||
ECarrierType.Electricity => c_ElectricityColor,
|
||||
_ => Colors.White
|
||||
};
|
||||
}
|
||||
|
||||
private static string PropLabel(PropState prop)
|
||||
{
|
||||
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.Door => "DOOR",
|
||||
EPropType.AllSeeingEyeTerminal => "EYE",
|
||||
EPropType.RemedySupply => RemedyShort(prop.RemedyType),
|
||||
EPropType.ReactorControl => "REACT",
|
||||
_ => string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
private static string CarrierShort(ECarrierType carrier)
|
||||
{
|
||||
return carrier switch {
|
||||
ECarrierType.Fuel => "F",
|
||||
ECarrierType.Coolant => "C",
|
||||
ECarrierType.Electricity => "E",
|
||||
_ => "?"
|
||||
};
|
||||
}
|
||||
|
||||
private static string RemedyShort(ERemedyType remedy)
|
||||
{
|
||||
return remedy switch {
|
||||
ERemedyType.FuelNeutralizer => "F REM",
|
||||
ERemedyType.CoolantNeutralizer => "C REM",
|
||||
ERemedyType.ElectricityNeutralizer => "E REM",
|
||||
ERemedyType.HeatShield => "H SHD",
|
||||
_ => "REM"
|
||||
};
|
||||
}
|
||||
|
||||
private static string ToolLabel(EEditorTool tool)
|
||||
{
|
||||
return tool switch {
|
||||
EEditorTool.FuelUnderground => "Fuel Net",
|
||||
EEditorTool.CoolantUnderground => "Coolant Net",
|
||||
EEditorTool.ElectricityUnderground => "Electric Net",
|
||||
EEditorTool.FuelFlow => "Fuel Source",
|
||||
EEditorTool.CoolantFlow => "Coolant Source",
|
||||
EEditorTool.ElectricityFlow => "Electric Source",
|
||||
EEditorTool.FuelConsumer => "Fuel Consumer",
|
||||
EEditorTool.CoolantConsumer => "Coolant Consumer",
|
||||
EEditorTool.ElectricityConsumer => "Electric Consumer",
|
||||
EEditorTool.AllSeeingEyeTerminal => "Eye Terminal",
|
||||
EEditorTool.FuelRemedySupply => "Fuel Remedy",
|
||||
EEditorTool.CoolantRemedySupply => "Coolant Remedy",
|
||||
EEditorTool.ElectricityRemedySupply => "Electric Remedy",
|
||||
EEditorTool.HeatRemedySupply => "Heat Shield",
|
||||
EEditorTool.ReactorControl => "Reactor",
|
||||
EEditorTool.FuelHazard => "Fuel Hazard",
|
||||
EEditorTool.CoolantHazard => "Coolant Hazard",
|
||||
EEditorTool.ElectricityHazard => "Electric Hazard",
|
||||
_ => tool.ToString()
|
||||
};
|
||||
}
|
||||
|
||||
private const double c_MinZoom = 0.5;
|
||||
private const double c_MaxZoom = 4;
|
||||
private const double c_ZoomStep = 1.15;
|
||||
private const double c_ClickPixelThreshold = 10;
|
||||
private static readonly Color c_FuelColor = ColorHelper.FromArgb(255, 220, 170, 68);
|
||||
private static readonly Color c_CoolantColor = ColorHelper.FromArgb(255, 57, 174, 196);
|
||||
private static readonly Color c_ElectricityColor = ColorHelper.FromArgb(255, 236, 226, 82);
|
||||
|
||||
private readonly SimulationEngine m_Simulation = new();
|
||||
private readonly Dictionary<ECellProp, CanvasBitmap> m_PropSprites = [];
|
||||
private readonly Dictionary<EPipeMedium, CanvasBitmap> m_PipeTilemaps = [];
|
||||
private StorageFile? m_CurrentFile;
|
||||
private LevelState m_Level;
|
||||
private IReadOnlyList<EditorToolViewModel> m_EditorTools = [];
|
||||
@@ -725,5 +718,4 @@ public sealed partial class MainWindow
|
||||
private CanvasBitmap? m_RobotSprite;
|
||||
private CanvasBitmap? m_LeakSprite;
|
||||
private CanvasBitmap? m_HeatSprite;
|
||||
private CanvasBitmap? m_FireSprite;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>
|
||||
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
|
||||
<RootNamespace>ReactorMaintenance.Win2D</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
@@ -13,6 +13,7 @@
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<EnableWindowsTargeting>true</EnableWindowsTargeting>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
|
||||
@@ -1,298 +1,203 @@
|
||||
using ReactorMaintenance.Simulation.Difficulties;
|
||||
using ReactorMaintenance.Simulation.Effects;
|
||||
using ReactorMaintenance.Simulation.Hazards;
|
||||
|
||||
namespace ReactorMaintenance.Simulation.Tests;
|
||||
|
||||
public sealed class SimulationEngineTests
|
||||
{
|
||||
[Fact]
|
||||
public void FuelLeakNearPoweredGeneratorCreatesIgnitionForecast()
|
||||
public void NetworkPropagationSuppliesBoundConsumersAndReadiesReactor()
|
||||
{
|
||||
var level = LevelState.Create("Fuel leak", 6, 6)
|
||||
.SetCell(new(2, 2), new() {
|
||||
Prop = ECellProp.Generator,
|
||||
Pipe = EPipeMedium.Fuel,
|
||||
LeakRate = Balancing.Current.FuelVaporFireThreshold,
|
||||
Pressure = Balancing.Current.PressurizedFuelLeakPressureThreshold + Balancing.Current.NeighborDistance,
|
||||
Integrity = Balancing.Current.DefaultEditedPipeIntegrity,
|
||||
Powered = true
|
||||
});
|
||||
|
||||
var forecasts = m_Engine.Forecast(level);
|
||||
|
||||
Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.Ignition && forecast.Position == new GridPosition(2, 2) && forecast.Turns == 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CoolantLeakOnPoweredCellRaisesElectricalCharge()
|
||||
{
|
||||
var level = LevelState.Create("Wet cable", 6, 6)
|
||||
.SetCell(new(3, 3), new() {
|
||||
Pipe = EPipeMedium.Coolant,
|
||||
LeakRate = Balancing.Current.ElectrifiedCoolantPoolingThreshold,
|
||||
Powered = true
|
||||
});
|
||||
var level = BuildReadyLevel();
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.True(next.GetCell(new(3, 3)).Hazards.ElectricalCharge >= Balancing.Current.ElectricalChargeIncrease);
|
||||
Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 2)).ServiceState);
|
||||
Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 3)).ServiceState);
|
||||
Assert.Equal(EConsumerServiceState.Producing, next.GetProp(new(3, 4)).ServiceState);
|
||||
Assert.Equal(ELevelState.Ready, next.Global.LevelState);
|
||||
Assert.Contains(next.Forecasts, forecast => forecast.Kind == EForecastKind.ReactorReady);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActiveFireSpreadsSmokeToOpenNeighbors()
|
||||
public void ReactorActivatesOnlyAtReadyControl()
|
||||
{
|
||||
var level = LevelState.Create("Smoke", 6, 6)
|
||||
.SetCell(new(2, 2), new() {
|
||||
Hazards = new() {
|
||||
Fire = true,
|
||||
Smoke = Balancing.Current.SmokeSpreadThreshold
|
||||
var level = m_Engine.AdvanceTurn(BuildReadyLevel()) with {
|
||||
Robot = new() { Position = new(5, 3) }
|
||||
};
|
||||
|
||||
var activated = m_Engine.ActivateReactor(level);
|
||||
|
||||
Assert.Equal(ELevelState.Won, activated.Global.LevelState);
|
||||
Assert.True(activated.Reactors[0].Activated);
|
||||
}
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void LeakingUndergroundCellInjectsMatchingSurfaceHazard()
|
||||
{
|
||||
var level = LevelState.Create("Leak", 6, 6);
|
||||
level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { State = EUndergroundState.Leaking, Amount = 5, Intensity = 5 }) with {
|
||||
Leaks = [new LeakState { Carrier = ECarrierType.Fuel, UndergroundPosition = new(2, 2), AccessPosition = new(2, 2) }]
|
||||
};
|
||||
level = level.SetProp(new(2, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.True(next.GetCell(new(2, 3)).Hazards.Smoke > 0);
|
||||
Assert.True(next.GetSurface(new(2, 2)).Fuel > 0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdvanceTurnRunsConfiguredCellEffects()
|
||||
public void ElementRemedyClearsHazardAndBlocksImmediateReentry()
|
||||
{
|
||||
var engine = new SimulationEngine([new TestCellEffect()], [], []);
|
||||
var level = LevelState.Create("Custom effect", 6, 6)
|
||||
.SetCell(new(2, 2), new() {
|
||||
Hazards = new() { Heat = 1 }
|
||||
});
|
||||
var level = LevelState.Create("Remedy", 6, 6);
|
||||
level = level.SetUnderground(new(2, 2), ECarrierType.Fuel, new() { State = EUndergroundState.Leaking, Amount = 5, Intensity = 5 });
|
||||
level = level.SetSurface(new(2, 2), new() { Fuel = 5 }) with {
|
||||
Robot = new() { Position = new(2, 2), FuelNeutralizers = 1 },
|
||||
Leaks = [new LeakState { Carrier = ECarrierType.Fuel, UndergroundPosition = new(2, 2), AccessPosition = new(2, 2) }]
|
||||
};
|
||||
|
||||
var next = engine.AdvanceTurn(level);
|
||||
var next = m_Engine.InteractLeak(level, ECarrierType.Fuel, true);
|
||||
|
||||
Assert.Equal(5, next.GetCell(new(2, 2)).Hazards.Heat);
|
||||
Assert.Equal(0, next.GetSurface(new(2, 2)).Fuel);
|
||||
Assert.True(next.GetSurface(new(2, 2)).FuelBlockTurns > 0);
|
||||
Assert.Equal(0, next.Robot.FuelNeutralizers);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AdvanceTurnRunsConfiguredAreaEffects()
|
||||
public void ClosedDoorBlocksAdjacentHeatFlow()
|
||||
{
|
||||
var engine = new SimulationEngine([], [new TestAreaEffect()], []);
|
||||
var level = LevelState.Create("Custom area effect", 6, 6);
|
||||
var level = LevelState.Create("Door", 6, 6);
|
||||
level = level.SetSurface(new(2, 2), new() { Heat = 8 }) with {
|
||||
Doors = [new DoorState { A = new(2, 2), B = new(3, 2), State = EDoorState.Closed }]
|
||||
};
|
||||
|
||||
var next = engine.AdvanceTurn(level);
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.Equal(7, next.GetCell(new(2, 2)).Hazards.Smoke);
|
||||
Assert.Equal(0, next.GetSurface(new(3, 2)).Heat);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OverpressurePredictsPipeBurst()
|
||||
public void HeatShieldPreventsRobotHeatLoss()
|
||||
{
|
||||
var level = LevelState.Create("Pressure", 6, 6)
|
||||
.SetCell(new(1, 2), new() {
|
||||
Pipe = EPipeMedium.Pressure,
|
||||
Pressure = 10,
|
||||
Integrity = 6
|
||||
});
|
||||
var level = LevelState.Create("Heat shield", 6, 6);
|
||||
level = level.SetSurface(new(2, 2), new() { Heat = Balancing.Current.RobotHeatSafetyThreshold }) with {
|
||||
Robot = new() { Position = new(2, 2), HeatImmunitySteps = 1 }
|
||||
};
|
||||
|
||||
var forecasts = m_Engine.Forecast(level);
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.PipeBurst && forecast.Turns == 2);
|
||||
Assert.NotEqual(ELevelState.Lost, next.Global.LevelState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForecastCapsFutureSimulationWhenNoTerminalConditionOccurs()
|
||||
public void RobotLosesOnUnsafeElementHazard()
|
||||
{
|
||||
var engine = new SimulationEngine([], [], [new StepCountingHazard()]);
|
||||
var level = LevelState.Create("Stable", 6, 6);
|
||||
var level = LevelState.Create("Unsafe", 6, 6);
|
||||
level = level.SetSurface(new(2, 2), new() { Electricity = Balancing.Current.MaxValue }) with {
|
||||
Robot = new() { Position = new(2, 2) }
|
||||
};
|
||||
|
||||
var forecasts = engine.Forecast(level);
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
|
||||
Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Count);
|
||||
Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Max(forecast => forecast.Turns));
|
||||
Assert.Equal(ELevelState.Lost, next.Global.LevelState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForecastUsesCurrentBalancingProfile()
|
||||
public void RuleEventCanCreateTerminalLossForecast()
|
||||
{
|
||||
var previous = Balancing.Current;
|
||||
try
|
||||
{
|
||||
Balancing.Current = new TestBalancing();
|
||||
var engine = new SimulationEngine([], [], [new StepCountingHazard()]);
|
||||
var level = LevelState.Create("Stable", 6, 6);
|
||||
|
||||
var forecasts = engine.Forecast(level);
|
||||
|
||||
Assert.Equal(Balancing.Current.MaxForecastStepCount, forecasts.Count);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Balancing.Current = previous;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForecastPredictsMeltdownFromFutureSimulation()
|
||||
{
|
||||
var level = LevelState.Create("Meltdown", 6, 6)
|
||||
.SetCell(new(2, 2), new() {
|
||||
Prop = ECellProp.Reactor,
|
||||
Hazards = new() { Heat = Balancing.Current.MeltdownCoreHeatThreshold - Balancing.Current.ReactorHeatIncrease }
|
||||
});
|
||||
|
||||
var forecasts = m_Engine.Forecast(level);
|
||||
|
||||
Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.Meltdown && forecast.Turns == 1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForecastReportsAlreadyLostLevelAtCurrentTurn()
|
||||
{
|
||||
var level = LevelState.Create("Lost", 6, 6) with {
|
||||
Global = new() {
|
||||
CoreHeat = Balancing.Current.MeltdownCoreHeatThreshold,
|
||||
Lost = true,
|
||||
Status = "CORE MELTDOWN"
|
||||
var level = LevelState.Create("Rule", 6, 6) with {
|
||||
RuleEvents = [
|
||||
new RuleEventState {
|
||||
Phase = ERuleEventPhase.EndOfTurn,
|
||||
ForecastText = "containment failure",
|
||||
Predicates = [new RulePredicate { Kind = ERulePredicateKind.TurnAtLeast, Turn = 0 }],
|
||||
Effects = [new RuleEffect { Kind = ERuleEffectKind.MarkTerminalLoss, Message = "CONTAINMENT FAILURE" }]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
var forecasts = m_Engine.Forecast(level);
|
||||
|
||||
Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.Meltdown && forecast.Turns == 0);
|
||||
Assert.Contains(forecasts, forecast => forecast.Kind == EForecastKind.RuleEvent && forecast.Message == "containment failure");
|
||||
Assert.Contains(forecasts, forecast => forecast.Kind == EForecastKind.TerminalLoss);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ForecastPredictsStabilityCollapseFromFutureSimulation()
|
||||
public void ValidatorRejectsWallHazardsAndInvalidReactorBinding()
|
||||
{
|
||||
var level = LevelState.Create("Collapse", 6, 6)
|
||||
.SetCell(new(2, 2), new() {
|
||||
Prop = ECellProp.Generator,
|
||||
Hazards = new() { Stability = Balancing.Current.CriticalCellStabilityThreshold }
|
||||
}) with {
|
||||
Global = new() { FacilityStability = Balancing.Current.FireStabilityDamage }
|
||||
var level = LevelState.Create("Invalid", 6, 6);
|
||||
level = level.SetTerrain(new(2, 2), ECellTerrain.Wall);
|
||||
level = level with {
|
||||
Surface = level.Surface.ToArray(),
|
||||
Reactors = [new ReactorBinding { ControlPosition = new(3, 3), FuelConsumerPosition = new(1, 1), CoolantConsumerPosition = new(1, 1), ElectricityConsumerPosition = new(1, 1) }]
|
||||
};
|
||||
level.Surface[level.Index(new(2, 2))] = new() { Heat = 1 };
|
||||
|
||||
var forecasts = m_Engine.Forecast(level);
|
||||
var report = new LevelValidator().Validate(level);
|
||||
|
||||
Assert.Contains(forecasts, forecast => forecast.Kind == EFailureKind.StabilityCollapse && forecast.Turns == 1);
|
||||
Assert.False(report.IsValid);
|
||||
Assert.Contains(report.Errors, error => error.Message.Contains("Wall cell", StringComparison.Ordinal));
|
||||
Assert.Contains(report.Errors, error => error.Message.Contains("Reactor binding", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StableReactorWithPowerAndCoolingCanActivate()
|
||||
public void LevelSerializationRoundTripsCurrentSchemaOnly()
|
||||
{
|
||||
var level = LevelState.Create("Ready", 8, 6)
|
||||
.SetCell(new(2, 2), new() {
|
||||
Prop = ECellProp.Reactor,
|
||||
Hazards = new() { Heat = 3 }
|
||||
})
|
||||
.SetCell(new(3, 2), new() {
|
||||
Prop = ECellProp.Generator,
|
||||
Powered = true
|
||||
})
|
||||
.SetCell(new(4, 2), new() {
|
||||
Prop = ECellProp.CoolingPump,
|
||||
Powered = true
|
||||
});
|
||||
|
||||
var next = m_Engine.AdvanceTurn(level);
|
||||
var activated = m_Engine.ActivateReactor(next);
|
||||
|
||||
Assert.Equal("REACTOR ONLINE", activated.Global.Status);
|
||||
Assert.True(activated.Global.ReactorActivated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LevelSerializationRoundTripsEditableState()
|
||||
{
|
||||
var level = LevelState.Create("Round trip", 5, 5);
|
||||
level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Reactor);
|
||||
level = LevelEditor.Apply(level, new(1, 2), EEditorTool.CoolantPipe);
|
||||
level = LevelEditor.Apply(level, new(1, 2), EEditorTool.Leak);
|
||||
var level = BuildReadyLevel();
|
||||
|
||||
var json = LevelSerializer.Serialize(level);
|
||||
var loaded = LevelSerializer.Deserialize(json);
|
||||
|
||||
Assert.Contains("\"Version\": 1", json);
|
||||
Assert.Contains("\"Version\": 2", json);
|
||||
Assert.Equal(level.Name, loaded.Name);
|
||||
Assert.Equal(ECellProp.Reactor, loaded.GetCell(new(2, 2)).Prop);
|
||||
Assert.Equal(EPipeMedium.Coolant, loaded.GetCell(new(1, 2)).Pipe);
|
||||
Assert.Equal(1, loaded.GetCell(new(1, 2)).LeakRate);
|
||||
Assert.Equal(EPropType.Flow, loaded.GetProp(new(2, 2)).Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LevelSerializationRejectsUnsupportedVersion()
|
||||
public void LevelSerializationRejectsOldSchema()
|
||||
{
|
||||
var json = """
|
||||
{
|
||||
"Version": 999,
|
||||
"Version": 1,
|
||||
"Level": {}
|
||||
}
|
||||
""";
|
||||
|
||||
var exception = Assert.Throws<InvalidOperationException>(() => LevelSerializer.Deserialize(json));
|
||||
|
||||
Assert.Contains("Unsupported level file version 999", exception.Message);
|
||||
Assert.Contains("Unsupported level file version 1", exception.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WallToolClearsCellPropsPipesAndHazards()
|
||||
private static LevelState BuildReadyLevel()
|
||||
{
|
||||
var level = LevelState.Create("Wall", 5, 5);
|
||||
level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Generator);
|
||||
level = LevelEditor.Apply(level, new(2, 2), EEditorTool.CoolantPipe);
|
||||
level = LevelEditor.Apply(level, new(2, 2), EEditorTool.Fire);
|
||||
|
||||
var edited = LevelEditor.Apply(level, new(2, 2), EEditorTool.Wall);
|
||||
var cell = edited.GetCell(new(2, 2));
|
||||
|
||||
Assert.Equal(ECellTerrain.Wall, cell.Terrain);
|
||||
Assert.Equal(ECellProp.None, cell.Prop);
|
||||
Assert.Equal(EPipeMedium.None, cell.Pipe);
|
||||
Assert.False(cell.Powered);
|
||||
Assert.False(cell.Hazards.Fire);
|
||||
var level = LevelState.Create("Ready", 8, 7);
|
||||
level = AddLine(level, ECarrierType.Fuel, new(2, 2), new(3, 2));
|
||||
level = AddLine(level, ECarrierType.Coolant, new(2, 3), new(3, 3));
|
||||
level = AddLine(level, ECarrierType.Electricity, new(2, 4), new(3, 4));
|
||||
level = level.SetProp(new(2, 2), new() { Type = EPropType.Flow, Carrier = ECarrierType.Fuel });
|
||||
level = level.SetProp(new(2, 3), new() { Type = EPropType.Flow, Carrier = ECarrierType.Coolant });
|
||||
level = level.SetProp(new(2, 4), new() { Type = EPropType.Flow, Carrier = ECarrierType.Electricity });
|
||||
level = level.SetProp(new(3, 2), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Fuel });
|
||||
level = level.SetProp(new(3, 3), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Coolant });
|
||||
level = level.SetProp(new(3, 4), new() { Type = EPropType.Consumer, Carrier = ECarrierType.Electricity });
|
||||
level = level.SetProp(new(5, 3), new() { Type = EPropType.ReactorControl, ReactorId = 1 });
|
||||
return level with {
|
||||
Robot = new() { Position = new(5, 3) },
|
||||
Reactors = [
|
||||
new ReactorBinding {
|
||||
ReactorId = 1,
|
||||
ControlPosition = new(5, 3),
|
||||
FuelConsumerPosition = new(3, 2),
|
||||
CoolantConsumerPosition = new(3, 3),
|
||||
ElectricityConsumerPosition = new(3, 4)
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PropToolsKeepFloorTerrain()
|
||||
private static LevelState AddLine(LevelState level, ECarrierType carrier, GridPosition a, GridPosition b)
|
||||
{
|
||||
var level = LevelState.Create("Prop", 5, 5);
|
||||
level = LevelEditor.Apply(level, new(1, 1), EEditorTool.Wall);
|
||||
|
||||
var edited = LevelEditor.Apply(level, new(1, 1), EEditorTool.Reactor);
|
||||
var cell = edited.GetCell(new(1, 1));
|
||||
|
||||
Assert.Equal(ECellTerrain.Floor, cell.Terrain);
|
||||
Assert.Equal(ECellProp.Reactor, cell.Prop);
|
||||
level = level.SetUnderground(a, carrier, new() { State = EUndergroundState.Intact });
|
||||
level = level.SetUnderground(b, carrier, new() { State = EUndergroundState.Intact });
|
||||
return level;
|
||||
}
|
||||
|
||||
private readonly SimulationEngine m_Engine = new();
|
||||
|
||||
private sealed class StepCountingHazard : Hazard
|
||||
{
|
||||
public override IEnumerable<Forecast> Predict(LevelState level, int turns)
|
||||
{
|
||||
yield return new(EFailureKind.PipeBurst, new(turns, 0), turns, $"STEP {turns}");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestBalancing : NormalBalancing
|
||||
{
|
||||
public override int MaxForecastStepCount => 2;
|
||||
}
|
||||
|
||||
private sealed class TestCellEffect : ISimulationEffect
|
||||
{
|
||||
public CellState Apply(CellState cell)
|
||||
{
|
||||
return cell with { Hazards = cell.Hazards with { Heat = 5 } };
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class TestAreaEffect : IAreaSimulationEffect
|
||||
{
|
||||
public CellState[] Apply(LevelState level, CellState[] cells)
|
||||
{
|
||||
var next = cells.ToArray();
|
||||
var position = new GridPosition(2, 2);
|
||||
var cell = next[level.Index(position)];
|
||||
next[level.Index(position)] = cell with { Hazards = cell.Hazards with { Smoke = 7 } };
|
||||
return next;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user