namespace Sand.Core; public sealed partial class SandSimulation { private void ApplyMovement(int x, int y, ushort typeId, bool leftFirst, ref uint seed) { if (_isStatic[typeId] != 0) { return; } var netForceX = 0f; var netForceY = 0f; if (_hasActiveFieldBounds || _settings.WindX != 0f || _settings.WindY != 0f) { var forceResponse = GetForceResponse(typeId); netForceX = GetNetForceX(x, y, typeId) * forceResponse; netForceY = GetNetForceY(x, y, typeId) * forceResponse; } if (netForceX > 0.25f) { leftFirst = false; } else if (netForceX < -0.25f) { leftFirst = true; } switch ((ParticleBehaviorKind)_behaviorKind[typeId]) { case ParticleBehaviorKind.Fire: MoveFire(x, y, leftFirst, ref seed, netForceX); return; case ParticleBehaviorKind.BurningWood: return; case ParticleBehaviorKind.Ember: MoveEmber(x, y, leftFirst, ref seed, netForceX); return; case ParticleBehaviorKind.Plasma: MovePlasma(x, y, leftFirst, ref seed, netForceX, netForceY); return; } switch ((ParticleKind)_kind[typeId]) { case ParticleKind.Solid: MoveSolid(x, y, typeId, leftFirst, ref seed, netForceX); break; case ParticleKind.Liquid: MoveLiquid(x, y, typeId, leftFirst, ref seed, netForceX); break; case ParticleKind.Gas: MoveGas(x, y, typeId, leftFirst, ref seed, netForceX, netForceY); break; } } private void MoveSolid(int x, int y, ushort typeId, bool leftFirst, ref uint seed, float netForceX) { if (TryMoveSolidDown(x, y, typeId)) { return; } if (!CanSlipSolid(typeId, ref seed, netForceX)) { return; } if (leftFirst) { if (TryMoveEmpty(x, y, x - 1, y + 1)) { return; } TryMoveEmpty(x, y, x + 1, y + 1); return; } if (TryMoveEmpty(x, y, x + 1, y + 1)) { return; } TryMoveEmpty(x, y, x - 1, y + 1); } private void MoveLiquid(int x, int y, ushort typeId, bool leftFirst, ref uint seed, float netForceX) { if (TryMoveLiquidDown(x, y, typeId)) { return; } var windPreferred = netForceX > 0.2f ? 1 : netForceX < -0.2f ? -1 : 0; if (windPreferred != 0 && CanFlowLiquid(typeId, ref seed, lateral: true, netForceX)) { if (TryMoveEmpty(x, y, x + windPreferred, y)) { return; } } var diagonalAllowed = CanFlowLiquid(typeId, ref seed, lateral: false, netForceX); var lateralAllowed = diagonalAllowed || CanFlowLiquid(typeId, ref seed, lateral: true, netForceX); if (leftFirst) { if (diagonalAllowed && TryMoveEmpty(x, y, x - 1, y + 1)) { return; } if (lateralAllowed && TryMoveEmpty(x, y, x - 1, y)) { return; } if (diagonalAllowed && TryMoveEmpty(x, y, x + 1, y + 1)) { return; } if (lateralAllowed && TryMoveEmpty(x, y, x + 1, y)) { return; } return; } if (diagonalAllowed && TryMoveEmpty(x, y, x + 1, y + 1)) { return; } if (lateralAllowed && TryMoveEmpty(x, y, x + 1, y)) { return; } if (diagonalAllowed && TryMoveEmpty(x, y, x - 1, y + 1)) { return; } if (lateralAllowed && TryMoveEmpty(x, y, x - 1, y)) { return; } } private void MoveGas(int x, int y, ushort typeId, bool leftFirst, ref uint seed, float netForceX, float netForceY) { if (CanRiseGas(typeId, ref seed, netForceY) && TryMoveEmpty(x, y, x, y - 1)) { return; } var windPreferred = netForceX > 0.15f ? 1 : netForceX < -0.15f ? -1 : 0; if (windPreferred != 0 && CanDriftGas(typeId, ref seed, netForceX, lateral: true) && TryMoveEmpty(x, y, x + windPreferred, y)) { return; } var diagonalAllowed = CanDriftGas(typeId, ref seed, netForceX, lateral: false); var lateralAllowed = diagonalAllowed || CanDriftGas(typeId, ref seed, netForceX, lateral: true); if (leftFirst) { if (diagonalAllowed && TryMoveEmpty(x, y, x - 1, y - 1)) { return; } if (lateralAllowed && TryMoveEmpty(x, y, x - 1, y)) { return; } if (diagonalAllowed && TryMoveEmpty(x, y, x + 1, y - 1)) { return; } if (lateralAllowed && TryMoveEmpty(x, y, x + 1, y)) { return; } return; } if (diagonalAllowed && TryMoveEmpty(x, y, x + 1, y - 1)) { return; } if (lateralAllowed && TryMoveEmpty(x, y, x + 1, y)) { return; } if (diagonalAllowed && TryMoveEmpty(x, y, x - 1, y - 1)) { return; } if (lateralAllowed && TryMoveEmpty(x, y, x - 1, y)) { return; } } private bool TryMoveInto(int x, int y, int nx, int ny, ushort typeId, bool swapAllowed) { if (!TryNormalizeCoordinate(ref nx, ref ny)) { return false; } if (_settings.OuterWall && typeId != _idWall && IsBoundary(nx, ny)) { return false; } var neighborId = TypeId[nx, ny]; if (neighborId == 0) { MoveCell(x, y, nx, ny); return true; } if (!swapAllowed) { return false; } var neighborKind = (ParticleKind)_kind[neighborId]; var movableNeighbor = neighborKind == ParticleKind.Liquid || neighborKind == ParticleKind.Gas; if (movableNeighbor && _mass[typeId] > _mass[neighborId]) { SwapCells(x, y, nx, ny); return true; } return false; } private bool TryMoveEmpty(int x, int y, int nx, int ny) { if (!TryNormalizeCoordinate(ref nx, ref ny)) { return false; } if (_settings.OuterWall && IsBoundary(nx, ny)) { return false; } if (TypeId[nx, ny] != 0) { return false; } MoveCell(x, y, nx, ny); return true; } private bool TryMoveSolidDown(int x, int y, ushort typeId) { var nx = x; var ny = y + 1; if (!TryNormalizeCoordinate(ref nx, ref ny)) { return false; } if (_settings.OuterWall && IsBoundary(nx, ny)) { return false; } var below = TypeId[nx, ny]; if (below == 0) { MoveCell(x, y, nx, ny); return true; } var belowKind = (ParticleKind)_kind[below]; if ((belowKind == ParticleKind.Liquid || belowKind == ParticleKind.Gas) && _mass[typeId] > _mass[below]) { SwapCells(x, y, nx, ny); return true; } return false; } private bool TryMoveLiquidDown(int x, int y, ushort typeId) { var nx = x; var ny = y + 1; if (!TryNormalizeCoordinate(ref nx, ref ny)) { return false; } if (_settings.OuterWall && IsBoundary(nx, ny)) { return false; } var below = TypeId[nx, ny]; if (below == 0) { MoveCell(x, y, nx, ny); return true; } var belowKind = (ParticleKind)_kind[below]; if (belowKind == ParticleKind.Gas && _mass[typeId] > _mass[below]) { SwapCells(x, y, nx, ny); return true; } if (_isMolten[typeId] != 0 && belowKind == ParticleKind.Liquid && _mass[typeId] > _mass[below]) { SwapCells(x, y, nx, ny); return true; } return false; } private void MoveFire(int x, int y, bool leftFirst, ref uint seed, float netForceX) { if (TryMoveEmpty(x, y, x, y - 1)) { return; } var preferredRight = netForceX > 0.05f ? true : netForceX < -0.05f ? false : !leftFirst; if (preferredRight) { if (TryMoveEmpty(x, y, x + 1, y - 1) || TryMoveEmpty(x, y, x + 1, y)) { return; } if (TryMoveEmpty(x, y, x - 1, y - 1) || TryMoveEmpty(x, y, x - 1, y)) { return; } } else { if (TryMoveEmpty(x, y, x - 1, y - 1) || TryMoveEmpty(x, y, x - 1, y)) { return; } if (TryMoveEmpty(x, y, x + 1, y - 1) || TryMoveEmpty(x, y, x + 1, y)) { return; } } if (NextChance(ref seed, 0.15f)) { ResetCell(x, y); } } private void MoveEmber(int x, int y, bool leftFirst, ref uint seed, float netForceX) { if (NextChance(ref seed, MathF.Max(0.1f, _upwardBias[TypeId[x, y]]))) { if (TryMoveEmpty(x, y, x, y - 1)) { return; } if (leftFirst) { if (TryMoveEmpty(x, y, x - 1, y - 1) || TryMoveEmpty(x, y, x - 1, y)) { return; } } else if (TryMoveEmpty(x, y, x + 1, y - 1) || TryMoveEmpty(x, y, x + 1, y)) { return; } } MoveSolid(x, y, TypeId[x, y], leftFirst, ref seed, netForceX); } private void MovePlasma(int x, int y, bool leftFirst, ref uint seed, float netForceX, float netForceY) { if (TryMoveEmpty(x, y, x, y - 1)) { return; } var lateralBias = MathF.Max(0.15f, _sideDriftBias[TypeId[x, y]]); if (NextChance(ref seed, lateralBias)) { var dir = netForceX > 0.05f ? 1 : netForceX < -0.05f ? -1 : leftFirst ? -1 : 1; if (TryMoveEmpty(x, y, x + dir, y - 1) || TryMoveEmpty(x, y, x + dir, y)) { return; } } MoveGas(x, y, TypeId[x, y], leftFirst, ref seed, netForceX, netForceY); } private float GetEffectiveVelocity(ushort typeId) { if (_velocity[typeId] > 0f) { return _velocity[typeId]; } return (ParticleKind)_kind[typeId] switch { ParticleKind.Solid => 0.45f, ParticleKind.Liquid => 0.35f, ParticleKind.Gas => 0.25f, _ => 0.35f, }; } private float GetForceResponse(ushort typeId) { var mobility = 0.35f + GetEffectiveVelocity(typeId); var damping = MathF.Max(0.25f, (_mass[typeId] * 0.85f) + (_friction[typeId] * 0.35f) + (_viscosity[typeId] * 0.45f)); return Clamp((mobility / damping) * _forceResponseMultiplier[typeId], 0.15f, 2f); } private bool CanSlipSolid(ushort typeId, ref uint seed, float netForceX) { if (MathF.Abs(netForceX) > 0.45f) { return true; } var chance = Clamp(0.2f + (GetEffectiveVelocity(typeId) * 0.95f) - (_friction[typeId] * 0.35f), 0.1f, 0.95f); return NextChance(ref seed, chance); } private bool CanFlowLiquid(ushort typeId, ref uint seed, bool lateral, float netForceX) { var forceBonus = MathF.Min(0.25f, MathF.Abs(netForceX) * 0.08f); var velocity = GetEffectiveVelocity(typeId); var baseChance = lateral ? 0.18f + (velocity * 0.95f) - (_viscosity[typeId] * 0.42f) - (_friction[typeId] * 0.08f) : 0.28f + (velocity * 0.9f) - (_viscosity[typeId] * 0.28f) - (_friction[typeId] * 0.05f); var flowScale = lateral ? _lateralFlowMultiplier[typeId] : _diagonalFlowMultiplier[typeId]; var chance = Clamp((baseChance * flowScale) + forceBonus, 0.03f, 0.98f); return NextChance(ref seed, chance); } private bool CanRiseGas(ushort typeId, ref uint seed, float netForceY) { if (netForceY < -0.65f) { return true; } var velocity = GetEffectiveVelocity(typeId); var buoyancy = MathF.Max(0f, -netForceY) * 0.08f; var chance = Clamp(0.1f + (velocity * 1.8f) - (_viscosity[typeId] * 0.22f) + buoyancy, 0.05f, 0.99f); return NextChance(ref seed, chance); } private bool CanDriftGas(ushort typeId, ref uint seed, float netForceX, bool lateral) { var forceBonus = MathF.Min(0.28f, MathF.Abs(netForceX) * 0.08f); var velocity = GetEffectiveVelocity(typeId); var baseChance = lateral ? 0.18f + (velocity * 1.15f) - (_viscosity[typeId] * 0.1f) : 0.22f + (velocity * 1.2f) - (_viscosity[typeId] * 0.16f); var chance = Clamp(baseChance + forceBonus - (_friction[typeId] * 0.04f), 0.05f, 0.99f); return NextChance(ref seed, chance); } }