sandpypi/Sand.Core/SandSimulation.Thermal.cs

610 lines
18 KiB
C#

namespace Sand.Core;
public sealed partial class SandSimulation
{
private bool TickLifetime(int x, int y, float dt)
{
var typeId = TypeId[x, y];
if (typeId == 0)
{
return false;
}
if (Lifetime[x, y] > 0f)
{
Lifetime[x, y] -= dt * 60f;
}
if (Lifetime[x, y] <= 0f && _defaultLifetime[typeId] > 0f)
{
if (typeId == _idSteam && TryCondenseSteam(x, y, ref typeId))
{
return true;
}
ResetCell(x, y);
return true;
}
return false;
}
private bool TickSpark(int x, int y, ushort typeId, ref uint seed)
{
var spark = SparkTime[x, y];
if (spark > 0)
{
SparkTime[x, y]--;
ForEachNeighbor8(x, y, (nx, ny) =>
{
var neighborId = TypeId[nx, ny];
if (neighborId != 0 && (_conductive[neighborId] != 0 || _conductivity[neighborId] > 0.5f) && SparkTime[nx, ny] == 0)
{
SparkTime[nx, ny] = 4;
MarkProcessed(nx, ny);
}
});
if (typeId == _idSpark)
{
ResetCell(x, y);
return true;
}
if (SparkTime[x, y] == 0)
{
SparkTime[x, y] = -10;
}
}
else if (spark < 0)
{
SparkTime[x, y]++;
}
if (typeId != _idEnergy)
{
return false;
}
TransferEnergy(x, y, typeId, ref seed, 0.3f);
return false;
}
private void AutoIgnite(int x, int y, ushort typeId)
{
if (Burning[x, y] == 0 && _burnTemperature[typeId] > 0f && Temperature[x, y] >= _burnTemperature[typeId] && _flamability[typeId] > 0f)
{
Burning[x, y] = 1;
BurnTime[x, y] = MathF.Max(_burnDuration[typeId], 1f);
}
}
private void ApplyRuntimeEmissionAndProduction(int x, int y, ushort typeId, ref uint seed)
{
var behavior = (ParticleBehaviorKind)_behaviorKind[typeId];
var isEffectEmitter =
behavior == ParticleBehaviorKind.Fire ||
behavior == ParticleBehaviorKind.BurningWood ||
behavior == ParticleBehaviorKind.Ember ||
behavior == ParticleBehaviorKind.Plasma;
var emission = _heatEmission[typeId] * _heatEmissionMultiplier[typeId];
if (behavior == ParticleBehaviorKind.Fire && emission <= 0f)
{
emission = 24f;
}
else if (behavior == ParticleBehaviorKind.Plasma && emission <= 0f)
{
emission = 80f;
}
else if (behavior == ParticleBehaviorKind.Ember && emission <= 0f)
{
emission = 12f;
}
else if (_isMolten[typeId] != 0 && emission <= 0f)
{
emission = typeId == _idLava ? 42f : 28f;
}
if (emission > 0f)
{
ApplyHeatEmission(x, y, emission);
}
var transfer = _energyTransfer[typeId] * _energyTransferMultiplier[typeId];
if ((behavior == ParticleBehaviorKind.Plasma || typeId == _idEnergy) && transfer <= 0f)
{
transfer = 100f;
}
if (transfer > 0f)
{
var chance = behavior == ParticleBehaviorKind.Plasma
? 0.9f
: MathF.Min(0.85f, 0.15f + (transfer / 100f) * 0.55f);
TransferEnergy(x, y, typeId, ref seed, chance);
if (behavior == ParticleBehaviorKind.Plasma)
{
ForEachNeighbor8(x, y, (nx, ny) =>
{
var neighborId = TypeId[nx, ny];
if (neighborId != 0 && (_conductive[neighborId] != 0 || _conductivity[neighborId] > 0.5f))
{
SparkTime[nx, ny] = 4;
MarkProcessed(nx, ny);
}
});
}
}
if (isEffectEmitter || Burning[x, y] != 0)
{
TrySpawnProducedParticle(x, y, typeId, ref seed);
}
if (_idSmoke != 0 && isEffectEmitter && _smokeSpawnChance[typeId] > 0f && NextChance(ref seed, _smokeSpawnChance[typeId]))
{
TrySpawnAtOffset(x, y, 0, -1, _idSmoke);
}
if (_idEmber != 0 && isEffectEmitter && _emberSpawnChance[typeId] > 0f && NextChance(ref seed, _emberSpawnChance[typeId]))
{
TrySpawnAtOffset(x, y, 0, -1, _idEmber);
}
}
private bool TickBurning(int x, int y, ushort typeId, ref uint seed)
{
if (Burning[x, y] == 0)
{
return false;
}
Temperature[x, y] += 1.5f + (_heatEmission[typeId] * 0.02f);
BurnTime[x, y] -= MathF.Max(0.05f, _burnDecayPerStep[typeId] * _burnRate[typeId]);
if (BurnTime[x, y] > 0f)
{
return false;
}
ResetCell(x, y);
return true;
}
private bool TickSpecialBehavior(int x, int y, ushort typeId, ref uint seed)
{
switch ((ParticleBehaviorKind)_behaviorKind[typeId])
{
case ParticleBehaviorKind.Fire:
if (Temperature[x, y] < 120f && NextChance(ref seed, 0.1f))
{
ResetCell(x, y);
return true;
}
break;
case ParticleBehaviorKind.Ember:
if (Temperature[x, y] < 60f && NextChance(ref seed, 0.1f))
{
ResetCell(x, y);
return true;
}
break;
case ParticleBehaviorKind.BurningWood:
if (_idEmber != 0 && NextChance(ref seed, _emberSpawnChance[typeId] * 0.5f))
{
TrySpawnAtOffset(x, y, 0, -1, _idEmber);
}
break;
}
return false;
}
private void ApplyPhaseTransitions(int x, int y, ref ushort typeId)
{
if (_phaseTransitionHysteresis[typeId] > 0f && _cellAge[x, y] < _phaseTransitionHysteresis[typeId])
{
return;
}
if (typeId == _idWater)
{
if (_idSteam != 0 && Temperature[x, y] >= 100f)
{
ReplaceCell(x, y, _idSteam);
typeId = TypeId[x, y];
return;
}
if (_idIce != 0 && Temperature[x, y] <= 0f)
{
ReplaceCell(x, y, _idIce);
typeId = TypeId[x, y];
return;
}
}
if (typeId == _idSteam && Temperature[x, y] <= 85f)
{
if (TryCondenseSteam(x, y, ref typeId))
{
return;
}
}
if ((typeId == _idIce || typeId == _idSnow) && _idWater != 0)
{
var meltPoint = typeId == _idIce ? 7f : 9f;
if (Temperature[x, y] >= meltPoint)
{
ReplaceCell(x, y, _idWater);
typeId = TypeId[x, y];
return;
}
}
if (_evapTarget[typeId] != 0 && Temperature[x, y] >= _evapTemperature[typeId])
{
ReplaceCell(x, y, _evapTarget[typeId]);
typeId = TypeId[x, y];
return;
}
if (_meltTarget[typeId] != 0 && Temperature[x, y] >= _meltTemperature[typeId])
{
ReplaceCell(x, y, _meltTarget[typeId]);
typeId = TypeId[x, y];
return;
}
if (_solidifyTarget[typeId] != 0 && Temperature[x, y] <= _solidifyTemperature[typeId])
{
ReplaceCell(x, y, _solidifyTarget[typeId]);
typeId = TypeId[x, y];
return;
}
if (_freezeTarget[typeId] != 0 && Temperature[x, y] <= _freezeTemperature[typeId])
{
ReplaceCell(x, y, _freezeTarget[typeId]);
typeId = TypeId[x, y];
}
}
private bool ApplyLocalReactions(int x, int y, ref ushort typeId, ref uint seed)
{
var neighbors = new (int X, int Y)[]
{
(x - 1, y),
(x + 1, y),
(x, y - 1),
(x, y + 1),
};
foreach (var (nx, ny) in neighbors)
{
var tx = nx;
var ty = ny;
if (!TryNormalizeCoordinate(ref tx, ref ty))
{
continue;
}
var neighborId = TypeId[tx, ty];
if (neighborId == 0)
{
continue;
}
if (typeId == _idWater && neighborId == _idSand && _idWetSand != 0)
{
ReplaceCell(tx, ty, _idWetSand);
ResetCell(x, y);
return true;
}
if (typeId == _idWater && neighborId == _idDirt && _idMud != 0)
{
ReplaceCell(tx, ty, _idMud);
ResetCell(x, y);
return true;
}
if (typeId == _idLava && neighborId == _idWater && _idStone != 0 && _idSteam != 0)
{
ReplaceCell(x, y, _idStone);
ReplaceCell(tx, ty, _idSteam);
typeId = TypeId[x, y];
return true;
}
if (IsFireLike(typeId) && IsWaterLike(neighborId))
{
ResetCell(x, y);
if (_idSteam != 0 && NextChance(ref seed, 0.5f))
{
ReplaceCell(tx, ty, _idSteam);
}
return true;
}
if ((IsFireLike(typeId) || typeId == _idLava) && neighborId != _idFire && neighborId != _idLava && _flamability[neighborId] > 0f && _idFire != 0)
{
var spreadChance = typeId == _idLava
? (_fireSpreadChance[typeId] > 0f ? _fireSpreadChance[typeId] : 0.1f)
: MathF.Max(0.05f, _fireSpreadChance[typeId]);
if (NextChance(ref seed, spreadChance))
{
ReplaceCell(tx, ty, _idFire);
}
}
if (typeId == _idAcid && neighborId != _idAcid && neighborId != _idFire && neighborId != _idSmoke && neighborId != _idWall && neighborId != _idGlass)
{
if (NextChance(ref seed, 0.05f))
{
ResetCell(tx, ty);
if (NextChance(ref seed, 0.2f))
{
ResetCell(x, y);
return true;
}
}
}
}
return false;
}
private bool TickPressure(int x, int y, ushort typeId, ref uint seed)
{
if (_airPressure[x, y] == 0f &&
_pressure[typeId] <= 0f &&
_explosive[typeId] == 0 &&
_pressureDuration[x, y] <= 0f &&
_integrity[x, y] >= _durability[typeId])
{
return false;
}
var localPressure = MathF.Abs(_airPressure[x, y]) * _pressureSensitivity[typeId];
localPressure += _pressure[typeId] * 0.1f;
// Mechanical pressure should come from compression/impacts, not from heat alone.
// Keep a small thermal contribution only for explicitly explosive materials.
if (_explosive[typeId] != 0 && Temperature[x, y] > _settings.AmbientTemperature + 250f)
{
localPressure += (Temperature[x, y] - _settings.AmbientTemperature) * 0.0015f;
}
var threshold = _pressureThreshold[typeId];
if (_explosive[typeId] != 0 && threshold <= 0f)
{
threshold = 8f;
}
else if (threshold <= 0f)
{
threshold = 2.5f + (_hardness[typeId] * 4f) + (_pressureResistance[typeId] * 0.35f);
}
threshold += _pressureTolerance[typeId] + _pressureResistance[typeId];
if (threshold <= 0f)
{
return false;
}
if (localPressure >= threshold)
{
_pressureDuration[x, y] += 1f;
}
else
{
_pressureDuration[x, y] = MathF.Max(0f, _pressureDuration[x, y] - 1f);
var maxIntegrity = _durability[typeId];
if (_integrity[x, y] < maxIntegrity)
{
_integrity[x, y] = MathF.Min(maxIntegrity, _integrity[x, y] + MathF.Max(0.2f, _hardness[typeId] * 0.1f));
}
return false;
}
var overload = MathF.Max(0f, localPressure - threshold);
var damageScale = 1f / MathF.Max(0.35f, 0.4f + _hardness[typeId] + (_pressureResistance[typeId] * 0.1f));
_integrity[x, y] -= MathF.Max(0.15f, 0.45f + overload) * damageScale;
var requiredDuration = _pressureThresholdDuration[typeId] > 0
? _pressureThresholdDuration[typeId]
: Math.Max(2, (int)MathF.Ceiling(2f + (_hardness[typeId] * 2f)));
if (_pressureDuration[x, y] < requiredDuration && _integrity[x, y] > 0f)
{
return false;
}
if (_explosive[typeId] != 0)
{
TriggerExplosion(x, y, typeId, ref seed);
ResetCell(x, y);
return true;
}
if (_flamability[typeId] > 0f && Temperature[x, y] >= Math.Max(60f, _burnTemperature[typeId] * 0.5f))
{
Burning[x, y] = 1;
BurnTime[x, y] = MathF.Max(1f, _burnDuration[typeId]);
return false;
}
if (_integrity[x, y] <= 0f || overload >= MathF.Max(1.5f, threshold * 0.45f))
{
return BreakCellFromPressure(x, y, typeId, ref seed);
}
return false;
}
private void ApplyHeatEmission(int x, int y, float amount)
{
Temperature[x, y] += amount * 0.08f;
ForEachNeighbor8(x, y, (nx, ny) =>
{
Temperature[nx, ny] += amount * 0.02f;
});
}
private void TransferEnergy(int x, int y, ushort typeId, ref uint seed, float chance)
{
var localSeed = seed;
ForEachNeighbor8(x, y, (nx, ny) =>
{
var neighborId = TypeId[nx, ny];
if (neighborId != 0 && (_conductive[neighborId] != 0 || _conductivity[neighborId] > 0.5f) && SparkTime[nx, ny] == 0 && NextChance(ref localSeed, chance))
{
SparkTime[nx, ny] = 4;
MarkProcessed(nx, ny);
}
else if (neighborId == 0 && _idSpark != 0 && NextChance(ref localSeed, MathF.Min(0.75f, chance * 0.5f)))
{
SetCell(nx, ny, _idSpark);
SparkTime[nx, ny] = 4;
}
});
seed = localSeed;
}
private void TrySpawnProducedParticle(int x, int y, ushort typeId, ref uint seed)
{
var produceType = _producesTarget[typeId];
if (produceType == 0)
{
return;
}
var chance = _produceChance[typeId];
if (chance <= 0f)
{
chance = 0.08f;
}
if (!NextChance(ref seed, chance))
{
return;
}
SpawnNeighborParticle(x, y, produceType, seed);
}
private bool TrySpawnAtOffset(int x, int y, int offsetX, int offsetY, ushort typeId)
{
var tx = x + offsetX;
var ty = y + offsetY;
if (!TryNormalizeCoordinate(ref tx, ref ty))
{
return false;
}
if ((_settings.OuterWall && IsBoundary(tx, ty)) || TypeId[tx, ty] != 0)
{
return false;
}
SetCell(tx, ty, typeId);
return true;
}
private bool IsFireLike(ushort typeId)
{
var behavior = (ParticleBehaviorKind)_behaviorKind[typeId];
return typeId == _idFire || behavior == ParticleBehaviorKind.Fire || behavior == ParticleBehaviorKind.Ember || behavior == ParticleBehaviorKind.Plasma;
}
private bool IsWaterLike(ushort typeId)
{
return typeId == _idWater || typeId == _idWetSand || typeId == _idMud;
}
private bool TryCondenseSteam(int x, int y, ref ushort typeId)
{
if (_idWater == 0)
{
return false;
}
var condensedTemperature = MathF.Max(_initialTemperature[_idWater], MathF.Min(Temperature[x, y], 95f));
ReplaceCell(x, y, _idWater);
Temperature[x, y] = condensedTemperature;
typeId = TypeId[x, y];
return true;
}
private void ApplyTemperatureDiffusion(int x, int y, ushort typeId)
{
var conductivity = MathF.Max(_conductivity[typeId], 0.01f);
var localTemperature = Temperature[x, y];
var airDelta = localTemperature - _settings.AmbientTemperature;
if (_settings.FastSim && MathF.Abs(airDelta) <= 0.5f)
{
var rightX = x + 1;
var rightY = y;
var downX = x;
var downY = y + 1;
var rightStable =
!TryNormalizeCoordinate(ref rightX, ref rightY) ||
TypeId[rightX, rightY] == 0 ||
MathF.Abs(localTemperature - Temperature[rightX, rightY]) <= 0.5f;
var downStable =
!TryNormalizeCoordinate(ref downX, ref downY) ||
TypeId[downX, downY] == 0 ||
MathF.Abs(localTemperature - Temperature[downX, downY]) <= 0.5f;
if (rightStable && downStable)
{
return;
}
}
if (MathF.Abs(airDelta) > 0.5f)
{
var transfer = Clamp(
airDelta * ((conductivity + _settings.AirConductivity) * 0.5f) * 0.1f * _ambientCoolingMultiplier[typeId] / _heatCapacity[typeId],
airDelta * -0.5f,
airDelta * 0.5f);
Temperature[x, y] -= transfer;
}
DiffuseBetween(x, y, x + 1, y, conductivity, _neighborHeatTransferMultiplier[typeId]);
DiffuseBetween(x, y, x, y + 1, conductivity, _neighborHeatTransferMultiplier[typeId]);
}
private void DiffuseBetween(int x, int y, int nx, int ny, float conductivity, float multiplier)
{
if (!TryNormalizeCoordinate(ref nx, ref ny))
{
return;
}
var neighborId = TypeId[nx, ny];
if (neighborId == 0)
{
return;
}
var neighborConductivity = MathF.Max(_conductivity[neighborId], 0.1f);
var delta = Temperature[x, y] - Temperature[nx, ny];
if (MathF.Abs(delta) <= 0.5f)
{
return;
}
var transfer = Clamp(
delta * ((conductivity + neighborConductivity) * 0.5f) * 0.1f * multiplier / MathF.Max(0.1f, _heatCapacity[TypeId[x, y]]),
delta * -0.5f,
delta * 0.5f);
Temperature[x, y] -= transfer;
Temperature[nx, ny] += transfer;
}
}