610 lines
18 KiB
C#
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;
|
|
}
|
|
}
|