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; } }