namespace Sand.Core; public sealed partial class SandSimulation { private void SetCell(int x, int y, ushort typeId, bool markProcessed = true, bool clearDynamics = false) { var wasEmpty = TypeId[x, y] == 0; if (wasEmpty) { ParticleCount++; } TypeId[x, y] = typeId; Temperature[x, y] = _initialTemperature[typeId]; BurnTime[x, y] = _burnDuration[typeId]; Burning[x, y] = _burningInit[typeId]; SparkTime[x, y] = 0; Lifetime[x, y] = _defaultLifetime[typeId]; _pressureDuration[x, y] = 0f; _cellAge[x, y] = 0f; _integrity[x, y] = _durability[typeId]; if (clearDynamics) { ClearDynamicFieldsAt(x, y); } ExpandOccupiedBoundsToInclude(x, y); MarkVisualDirty(x, y); if (markProcessed) { MarkProcessed(x, y); } } private void ForceSetCell(int x, int y, ushort typeId) { var wasEmpty = TypeId[x, y] == 0; if (wasEmpty) { ParticleCount++; } TypeId[x, y] = typeId; Temperature[x, y] = _initialTemperature[typeId]; BurnTime[x, y] = _burnDuration[typeId]; Burning[x, y] = _burningInit[typeId]; SparkTime[x, y] = 0; Lifetime[x, y] = _defaultLifetime[typeId]; _pressureDuration[x, y] = 0f; _cellAge[x, y] = 0f; _integrity[x, y] = _durability[typeId]; ExpandOccupiedBoundsToInclude(x, y); MarkVisualDirty(x, y); } private void ReplaceCell(int x, int y, ushort typeId) { TypeId[x, y] = typeId; Temperature[x, y] = _initialTemperature[typeId]; BurnTime[x, y] = _burnDuration[typeId]; Burning[x, y] = _burningInit[typeId]; SparkTime[x, y] = 0; Lifetime[x, y] = _defaultLifetime[typeId]; _pressureDuration[x, y] = 0f; _cellAge[x, y] = 0f; _integrity[x, y] = _durability[typeId]; ExpandOccupiedBoundsToInclude(x, y); MarkVisualDirty(x, y); MarkProcessed(x, y); } private void ResetCell(int x, int y) { var typeId = TypeId[x, y]; if (typeId != 0) { TrySpawnOnDeath(x, y, typeId); } if (TypeId[x, y] != 0) { ParticleCount = Math.Max(0, ParticleCount - 1); } MarkBoundsDirtyIfRemovingOccupiedCell(x, y); ResetCellWithoutCountChange(x, y); MarkVisualDirty(x, y); } private void MoveCell(int x, int y, int nx, int ny) { TypeId[nx, ny] = TypeId[x, y]; Temperature[nx, ny] = Temperature[x, y]; BurnTime[nx, ny] = BurnTime[x, y]; Burning[nx, ny] = Burning[x, y]; SparkTime[nx, ny] = SparkTime[x, y]; Lifetime[nx, ny] = Lifetime[x, y]; _pressureDuration[nx, ny] = _pressureDuration[x, y]; _cellAge[nx, ny] = _cellAge[x, y]; _integrity[nx, ny] = _integrity[x, y]; MarkBoundsDirtyIfRemovingOccupiedCell(x, y); ResetCellWithoutCountChange(x, y); ExpandOccupiedBoundsToInclude(nx, ny); MarkVisualDirty(x, y); MarkVisualDirty(nx, ny); MarkProcessed(nx, ny); InjectMovementWind(x, y, nx, ny, TypeId[nx, ny]); InjectMovementPressure(x, y, nx, ny, TypeId[nx, ny]); } private void SwapCells(int x, int y, int nx, int ny) { (TypeId[x, y], TypeId[nx, ny]) = (TypeId[nx, ny], TypeId[x, y]); (Temperature[x, y], Temperature[nx, ny]) = (Temperature[nx, ny], Temperature[x, y]); (BurnTime[x, y], BurnTime[nx, ny]) = (BurnTime[nx, ny], BurnTime[x, y]); (Burning[x, y], Burning[nx, ny]) = (Burning[nx, ny], Burning[x, y]); (SparkTime[x, y], SparkTime[nx, ny]) = (SparkTime[nx, ny], SparkTime[x, y]); (Lifetime[x, y], Lifetime[nx, ny]) = (Lifetime[nx, ny], Lifetime[x, y]); (_pressureDuration[x, y], _pressureDuration[nx, ny]) = (_pressureDuration[nx, ny], _pressureDuration[x, y]); (_cellAge[x, y], _cellAge[nx, ny]) = (_cellAge[nx, ny], _cellAge[x, y]); (_integrity[x, y], _integrity[nx, ny]) = (_integrity[nx, ny], _integrity[x, y]); ExpandOccupiedBoundsToInclude(x, y); ExpandOccupiedBoundsToInclude(nx, ny); MarkVisualDirty(x, y); MarkVisualDirty(nx, ny); MarkProcessed(x, y); MarkProcessed(nx, ny); } private void ResetCellWithoutCountChange(int x, int y) { TypeId[x, y] = 0; Temperature[x, y] = _settings.AmbientTemperature; BurnTime[x, y] = 0f; Burning[x, y] = 0; SparkTime[x, y] = 0; Lifetime[x, y] = 0f; _pressureDuration[x, y] = 0f; _cellAge[x, y] = 0f; _integrity[x, y] = 0f; MarkVisualDirty(x, y); } private void DecayFields(float dt) { if (!TryGetSimulationBounds(out var startX, out var startY, out var endX, out var endY)) { return; } var windDecay = MathF.Max(0f, 1f - (dt * 3.5f)); var forceDecay = MathF.Max(0f, 1f - (dt * 1.2f)); var pressureDecay = MathF.Max(0f, 1f - (dt * 4.5f)); _hasActiveFieldBounds = false; for (var y = startY; y <= endY; y++) { for (var x = startX; x <= endX; x++) { _windFieldX[x, y] *= windDecay; _windFieldY[x, y] *= windDecay; _forceFieldX[x, y] *= forceDecay; _forceFieldY[x, y] *= forceDecay; var localPressureDecay = pressureDecay * _pressureDecayMultiplier[Math.Min(TypeId[x, y], _pressureDecayMultiplier.Length - 1)]; _airPressure[x, y] *= localPressureDecay; if (MathF.Abs(_windFieldX[x, y]) < 0.02f) { _windFieldX[x, y] = 0f; } if (MathF.Abs(_windFieldY[x, y]) < 0.02f) { _windFieldY[x, y] = 0f; } if (MathF.Abs(_forceFieldX[x, y]) < 0.02f) { _forceFieldX[x, y] = 0f; } if (MathF.Abs(_forceFieldY[x, y]) < 0.02f) { _forceFieldY[x, y] = 0f; } if (MathF.Abs(_airPressure[x, y]) < 0.02f) { _airPressure[x, y] = 0f; } if (TypeId[x, y] != 0) { _cellAge[x, y] += dt * 60f; var maxIntegrity = _durability[TypeId[x, y]]; if (_integrity[x, y] < maxIntegrity) { _integrity[x, y] = MathF.Min(maxIntegrity, _integrity[x, y] + (dt * 60f * MathF.Max(0.02f, _hardness[TypeId[x, y]] * 0.03f))); } } if (HasDynamicFieldAt(x, y)) { ExpandActiveFieldBoundsToInclude(x, y); } } } _fieldBoundsDirty = false; if (AreFieldVisualsEnabled()) { MarkVisualDirtyRect(startX, startY, endX, endY); } } private void InjectMovementWind(int x, int y, int nx, int ny, ushort typeId) { if (typeId == 0) { return; } var kind = (ParticleKind)_kind[typeId]; if (kind != ParticleKind.Solid && kind != ParticleKind.Liquid) { return; } var moveX = nx - x; var moveY = ny - y; if (moveX == 0 && moveY == 0) { return; } var strength = kind == ParticleKind.Solid ? 0.55f : 0.35f; var impulseX = moveX * strength; var impulseY = moveY * strength * 0.5f; AddWindImpulse(nx, ny, impulseX * 0.5f, impulseY); AddWindImpulse(nx - Math.Sign(moveX == 0 ? 1 : moveX), ny, impulseX * 0.25f, impulseY * 0.35f); AddWindImpulse(nx + Math.Sign(moveX == 0 ? 1 : moveX), ny, impulseX * 0.25f, impulseY * 0.35f); AddWindImpulse(nx, ny - 1, impulseX * 0.15f, impulseY * 0.2f); } private void AddWindImpulse(int x, int y, float impulseX, float impulseY) { if (!TryNormalizeCoordinate(ref x, ref y)) { return; } if (_settings.OuterWall && IsBoundary(x, y)) { return; } var hadDynamicBefore = HasDynamicFieldAt(x, y); _windFieldX[x, y] = Math.Clamp(_windFieldX[x, y] + impulseX, -8f, 8f); _windFieldY[x, y] = Math.Clamp(_windFieldY[x, y] + impulseY, -8f, 8f); TrackDynamicFieldMutation(x, y, hadDynamicBefore); } private void InjectMovementPressure(int x, int y, int nx, int ny, ushort typeId) { if (typeId == 0) { return; } var kind = (ParticleKind)_kind[typeId]; if (kind != ParticleKind.Solid && kind != ParticleKind.Liquid) { return; } var moveY = ny - y; if (moveY <= 0) { return; } var sourceAge = _cellAge[nx, ny]; var impulse = ComputeMovementPressureImpulse(typeId, (ParticleKind)_kind[typeId], moveY, sourceAge); if (impulse <= 0f) { return; } AddPressureImpulse(nx, ny, impulse); AddPressureImpulse(nx - 1, ny, impulse * 0.3f); AddPressureImpulse(nx + 1, ny, impulse * 0.3f); } private void AddPressureImpulse(int x, int y, float amount) { if (!TryNormalizeCoordinate(ref x, ref y)) { return; } if (_settings.OuterWall && IsBoundary(x, y)) { return; } var hadDynamicBefore = HasDynamicFieldAt(x, y); _airPressure[x, y] = Math.Clamp(_airPressure[x, y] + amount, -10f, 10f); TrackDynamicFieldMutation(x, y, hadDynamicBefore); } private void TriggerExplosion(int x, int y, ushort typeId, ref uint seed) { var marker = _activeStepToken != 0 ? _activeStepToken : Frame + 1; if (!InBounds(x, y)) { return; } if (_explosionFrame[x, y] == marker) { return; } var pending = new Stack<(int X, int Y, ushort TypeId)>(); _explosionFrame[x, y] = marker; pending.Push((x, y, typeId)); while (pending.Count > 0) { var current = pending.Pop(); var cx = current.X; var cy = current.Y; var currentTypeId = current.TypeId; var radius = Math.Max(1, (int)_explosionRadius[currentTypeId]); var force = MathF.Max(2f, _explosionForce[currentTypeId]); for (var dx = -radius; dx <= radius; dx++) { for (var dy = -radius; dy <= radius; dy++) { var distanceSq = (dx * dx) + (dy * dy); if (distanceSq > radius * radius) { continue; } var tx = cx + dx; var ty = cy + dy; if (!TryNormalizeCoordinate(ref tx, ref ty)) { continue; } if (_settings.OuterWall && IsBoundary(tx, ty)) { continue; } var distance = MathF.Max(1f, MathF.Sqrt(distanceSq)); var falloff = 1f - (distance / radius); var hadDynamicBefore = HasDynamicFieldAt(tx, ty); _airPressure[tx, ty] = Math.Clamp(_airPressure[tx, ty] + (force * falloff * 2.5f), -10f, 10f); Temperature[tx, ty] += force * falloff * 25f; if (distanceSq > 0) { _forceFieldX[tx, ty] = Math.Clamp(_forceFieldX[tx, ty] + (dx / distance) * force * falloff, -8f, 8f); _forceFieldY[tx, ty] = Math.Clamp(_forceFieldY[tx, ty] + (dy / distance) * force * falloff, -8f, 8f); } TrackDynamicFieldMutation(tx, ty, hadDynamicBefore); var neighborId = TypeId[tx, ty]; if (neighborId == 0) { if (_idSmoke != 0 && NextChance(ref seed, 0.08f)) { SetCell(tx, ty, _idSmoke); } else if (_idFire != 0 && NextChance(ref seed, 0.05f)) { SetCell(tx, ty, _idFire); } continue; } if (neighborId == _idWall || neighborId == _idGlass) { continue; } if (_explosive[neighborId] != 0 && NextChance(ref seed, 0.25f)) { if (_explosionFrame[tx, ty] != marker) { _explosionFrame[tx, ty] = marker; pending.Push((tx, ty, neighborId)); } ResetCell(tx, ty); continue; } if (_flamability[neighborId] > 0f && _idFire != 0 && NextChance(ref seed, 0.12f)) { ReplaceCell(tx, ty, _idFire); continue; } if (_mass[neighborId] < 0.5f && NextChance(ref seed, 0.18f + falloff * 0.22f)) { var brokenType = _brokenTarget[neighborId]; if (brokenType != 0 && brokenType != neighborId) { ReplaceCell(tx, ty, brokenType); } else { ResetCell(tx, ty); } } } } } } private void ApplyRadialForceBrushAtPixel(int centerX, int centerY, int brushRadius, float strength, bool inward, ToolProfile profile) { var gridCenterX = centerX / ParticleSize; var gridCenterY = centerY / ParticleSize; var effectiveRadius = Math.Max(brushRadius, profile.RadiusCells); for (var dx = -effectiveRadius; dx <= effectiveRadius; dx++) { for (var dy = -effectiveRadius; dy <= effectiveRadius; dy++) { var radiusSq = (dx * dx) + (dy * dy); if (radiusSq == 0 || radiusSq > effectiveRadius * effectiveRadius) { continue; } var x = gridCenterX + dx; var y = gridCenterY + dy; if (!TryNormalizeCoordinate(ref x, ref y)) { continue; } if (_settings.OuterWall && IsBoundary(x, y)) { continue; } var distance = MathF.Sqrt(radiusSq); if (!ToolAffectsCell(profile.Affects, x, y)) { continue; } var falloff = ComputeFalloff(distance, effectiveRadius, profile.Falloff); var dirX = dx / distance; var dirY = dy / distance; if (inward) { dirX = -dirX; dirY = -dirY; } var turbulence = SampleTurbulence(x, y, profile.Turbulence); var hadDynamicBefore = HasDynamicFieldAt(x, y); _forceFieldX[x, y] = Math.Clamp(_forceFieldX[x, y] + (dirX + turbulence.X) * falloff * strength * profile.Strength, -8f, 8f); _forceFieldY[x, y] = Math.Clamp(_forceFieldY[x, y] + (dirY + turbulence.Y) * falloff * strength * profile.Strength, -8f, 8f); TrackDynamicFieldMutation(x, y, hadDynamicBefore); } } } private float ComputeMovementPressureImpulse(ushort typeId, ParticleKind kind, int moveY, float cellAge) { if (moveY <= 0 || cellAge < 4f) { return 0f; } var impact = MathF.Max(0f, _mass[typeId] - 1.1f) * 0.08f; impact *= MathF.Max(0.45f, _velocity[typeId] + 0.4f); if (kind == ParticleKind.Liquid) { impact *= 0.3f; } else if (kind == ParticleKind.Solid) { impact *= _isStatic[typeId] != 0 ? 0f : 0.55f; } if (_isMolten[typeId] != 0) { impact *= 0.45f; } return Math.Clamp(impact, 0f, 0.4f); } private float GetNetForceX(int x, int y, ushort typeId) { var pressureGradient = SamplePressure(x - 1, y) - SamplePressure(x + 1, y); var pressureInfluence = GetPressureForceInfluence(typeId, x, y); return _settings.WindX + _windFieldX[x, y] + _forceFieldX[x, y] + (pressureGradient * pressureInfluence); } private float GetNetForceY(int x, int y, ushort typeId) { var pressureGradient = SamplePressure(x, y + 1) - SamplePressure(x, y - 1); var pressureInfluence = GetPressureForceInfluence(typeId, x, y) * 0.85f; return _settings.WindY + _windFieldY[x, y] + _forceFieldY[x, y] + (pressureGradient * pressureInfluence); } private float GetPressureForceInfluence(ushort typeId, int x, int y) { if (_cellAge[x, y] < 4f) { return 0f; } return ((ParticleBehaviorKind)_behaviorKind[typeId], (ParticleKind)_kind[typeId]) switch { (ParticleBehaviorKind.Fire, _) => 0.09f, (ParticleBehaviorKind.Plasma, _) => 0.1f, (_, ParticleKind.Gas) => 0.08f, (_, ParticleKind.Liquid) => 0.025f, (_, ParticleKind.Solid) => _isStatic[typeId] != 0 ? 0f : 0.015f, _ => 0.03f, }; } private float SamplePressure(int x, int y) { if (!TryNormalizeCoordinate(ref x, ref y)) { return 0f; } return _airPressure[x, y]; } }