1062 lines
33 KiB
C#
1062 lines
33 KiB
C#
using Sand.Core;
|
|
|
|
namespace Sand.ChunkPrototype;
|
|
|
|
public sealed partial class PrototypeSparseSandAdapter
|
|
{
|
|
private void TryStepParticle(ChunkCoord coord, int localX, int localY, PrototypeParticle particle)
|
|
{
|
|
var worldX = ToWorldX(coord, localX);
|
|
var worldY = ToWorldY(coord, localY);
|
|
var seed = unchecked(_stepCounter + (worldX * 31) + (worldY * 131));
|
|
if (CanUseMovementFastPath(worldX, worldY, particle))
|
|
{
|
|
ApplyMovementOnly(coord, localX, localY, particle, ref seed, worldX, worldY);
|
|
return;
|
|
}
|
|
|
|
_fullRuntimeStepCount++;
|
|
switch (particle.Kind)
|
|
{
|
|
case ParticleKind.Solid:
|
|
_fullRuntimeSolidCount++;
|
|
break;
|
|
case ParticleKind.Liquid:
|
|
_fullRuntimeLiquidCount++;
|
|
break;
|
|
case ParticleKind.Gas:
|
|
_fullRuntimeGasCount++;
|
|
break;
|
|
}
|
|
|
|
if (NeedsRuntimeAging(particle))
|
|
{
|
|
TickCellAging(worldX, worldY, particle);
|
|
}
|
|
|
|
if (particle.HasLifetimeBehavior && TickLifetime(worldX, worldY, ref particle))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (particle.HasBurnBehavior)
|
|
{
|
|
AutoIgnite(worldX, worldY, particle);
|
|
}
|
|
|
|
if (NeedsRuntimeEmission(particle))
|
|
{
|
|
ApplyRuntimeEmissionAndProduction(worldX, worldY, particle, ref seed);
|
|
}
|
|
|
|
if (particle.HasBurnBehavior && TickBurning(worldX, worldY, ref particle))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (particle.HasSpecialBehavior && TickSpecialBehavior(worldX, worldY, particle, ref seed))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (particle.HasPhaseBehavior)
|
|
{
|
|
ApplyPhaseTransition(worldX, worldY, ref particle);
|
|
if (!TryRefreshRuntimeParticle(worldX, worldY, ref particle))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (particle.HasReactionBehavior && ApplyLocalReaction(worldX, worldY, particle, ref seed))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (particle.HasReactionBehavior && !TryRefreshRuntimeParticle(worldX, worldY, ref particle))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (particle.HasPressureBehavior && ApplyPressureResponse(worldX, worldY, particle))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (particle.HasPressureBehavior && !TryRefreshRuntimeParticle(worldX, worldY, ref particle))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (particle.HasThermalBehavior)
|
|
{
|
|
ApplyTemperatureDiffusion(worldX, worldY, particle);
|
|
}
|
|
|
|
if (particle.IsStatic)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ApplyMovementOnly(coord, localX, localY, particle, ref seed, worldX, worldY);
|
|
}
|
|
|
|
private static bool NeedsRuntimeAging(PrototypeParticle particle) =>
|
|
particle.PhaseTransitionHysteresis > 0f || particle.HasPressureBehavior;
|
|
|
|
private static bool NeedsRuntimeEmission(PrototypeParticle particle) =>
|
|
particle.HasEmissionBehavior || particle.IsMolten;
|
|
|
|
private bool TryRefreshRuntimeParticle(int x, int y, ref PrototypeParticle particle) =>
|
|
TryGetParticle(x, y, out particle);
|
|
|
|
private void ApplyMovementOnly(ChunkCoord coord, int localX, int localY, PrototypeParticle particle, ref int seed, int worldX, int worldY)
|
|
{
|
|
if (particle.IsStatic)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var (netForceX, netForceY) = GetNetForce(worldX, worldY, particle);
|
|
netForceX *= GetForceResponse(particle);
|
|
netForceY *= GetForceResponse(particle);
|
|
var horizontalPreference = ResolveHorizontalPreference(coord, localX, localY, particle, netForceX);
|
|
var leftFirst = horizontalPreference < 0;
|
|
if (netForceX > 0.25f)
|
|
{
|
|
leftFirst = false;
|
|
}
|
|
else if (netForceX < -0.25f)
|
|
{
|
|
leftFirst = true;
|
|
}
|
|
|
|
switch (particle.BehaviorKind)
|
|
{
|
|
case ParticleBehaviorKind.Fire:
|
|
MoveFire(worldX, worldY, leftFirst, ref seed, netForceX);
|
|
return;
|
|
case ParticleBehaviorKind.BurningWood:
|
|
return;
|
|
case ParticleBehaviorKind.Ember:
|
|
MoveEmber(worldX, worldY, particle, leftFirst, ref seed, netForceX);
|
|
return;
|
|
case ParticleBehaviorKind.Plasma:
|
|
MovePlasma(worldX, worldY, particle, leftFirst, ref seed, netForceX, netForceY);
|
|
return;
|
|
}
|
|
|
|
switch (particle.Kind)
|
|
{
|
|
case ParticleKind.Solid:
|
|
TryMoveSolid(coord, localX, localY, worldX, worldY, particle, leftFirst, ref seed, netForceX);
|
|
break;
|
|
case ParticleKind.Liquid:
|
|
TryMoveLiquid(worldX, worldY, particle, leftFirst, ref seed, netForceX);
|
|
break;
|
|
case ParticleKind.Gas:
|
|
TryMoveGas(worldX, worldY, particle, leftFirst, ref seed, netForceX, netForceY);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private void StepMovementOnlyParticle(ChunkCoord coord, int localX, int localY, PrototypeParticle particle)
|
|
{
|
|
_movementOnlyFastPathCount++;
|
|
var worldX = ToWorldX(coord, localX);
|
|
var worldY = ToWorldY(coord, localY);
|
|
var seed = unchecked(_stepCounter + (worldX * 31) + (worldY * 131));
|
|
ApplyMovementOnly(coord, localX, localY, particle, ref seed, worldX, worldY);
|
|
}
|
|
|
|
private bool CanUseMovementFastPath(int x, int y, PrototypeParticle particle)
|
|
{
|
|
if (particle.IsStatic)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!particle.RequiresFullRuntimeStep)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (particle.HasLifetimeBehavior || particle.HasBurnBehavior || particle.HasEmissionBehavior || particle.HasSpecialBehavior)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (particle.HasPressureBehavior && (GetLocalPressure(x, y) > 0.05f || GetFieldForceMagnitude(x, y) > 0.08f || GetPressureDurationAt(x, y) > 0f || GetIntegrityAt(x, y) < particle.Durability))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (particle.HasPhaseBehavior)
|
|
{
|
|
var temperature = GetTemperatureAt(x, y);
|
|
if ((particle.MeltTypeId != 0 && temperature >= particle.MeltTemperature) ||
|
|
(particle.EvaporateTypeId != 0 && temperature >= particle.EvaporateTemperature) ||
|
|
(particle.FreezeTypeId != 0 && temperature <= particle.FreezeTemperature) ||
|
|
(particle.SolidifyTypeId != 0 &&
|
|
(temperature <= particle.SolidifyTemperature ||
|
|
(particle.MotionType == PrototypeParticleType.Steam && GetPressureLoad(x, y) >= MathF.Max(0.9f, particle.PressureThreshold * 0.35f)))))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (particle.HasReactionBehavior && HasPotentialReactiveNeighbor(x, y, particle))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (particle.HasThermalBehavior)
|
|
{
|
|
var temperature = GetTemperatureAt(x, y);
|
|
if (CanThrottleGasRuntime(x, y, particle, temperature))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (MathF.Abs(temperature - _ambientTemperature) > 0.5f)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool TryMoveSolid(ChunkCoord coord, int localX, int localY, int x, int y, PrototypeParticle particle, bool leftFirst, ref int seed, float netForceX)
|
|
{
|
|
if (TrySkipBlockedSolid(coord, localX, localY, netForceX))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (TryMoveSolidDown(x, y, particle))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var canMoveLeft = CanMoveIntoEmpty(x - 1, y + 1);
|
|
var canMoveRight = CanMoveIntoEmpty(x + 1, y + 1);
|
|
if (!canMoveLeft && !canMoveRight)
|
|
{
|
|
CacheBlockedSolid(coord, localX, localY);
|
|
return false;
|
|
}
|
|
|
|
if (!CanSlipSolid(particle, ref seed, netForceX))
|
|
{
|
|
RecordStalledMovable();
|
|
KeepChunkAwakeAt(x, y);
|
|
return false;
|
|
}
|
|
|
|
if (leftFirst)
|
|
{
|
|
if (canMoveLeft && TryMoveEmpty(x, y, x - 1, y + 1))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (canMoveRight && TryMoveEmpty(x, y, x + 1, y + 1))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (canMoveRight && TryMoveEmpty(x, y, x + 1, y + 1))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (canMoveLeft && TryMoveEmpty(x, y, x - 1, y + 1))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
RecordStalledMovable();
|
|
KeepChunkAwakeAt(x, y);
|
|
return false;
|
|
}
|
|
|
|
private bool TryMoveLiquid(int x, int y, PrototypeParticle particle, bool leftFirst, ref int seed, float netForceX)
|
|
{
|
|
if (TryMoveLiquidDown(x, y, particle))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var windPreferred = netForceX > 0.2f ? 1 : netForceX < -0.2f ? -1 : 0;
|
|
if (windPreferred != 0 && CanFlowLiquid(particle, ref seed, lateral: true, netForceX) && TryMoveEmpty(x, y, x + windPreferred, y))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var diagonalAllowed = CanFlowLiquid(particle, ref seed, lateral: false, netForceX);
|
|
var lateralAllowed = diagonalAllowed || CanFlowLiquid(particle, ref seed, lateral: true, netForceX);
|
|
var openLeftDiagonal = CanMoveIntoEmpty(x - 1, y + 1);
|
|
var openRightDiagonal = CanMoveIntoEmpty(x + 1, y + 1);
|
|
var openLeftLateral = CanMoveIntoEmpty(x - 1, y);
|
|
var openRightLateral = CanMoveIntoEmpty(x + 1, y);
|
|
if (leftFirst)
|
|
{
|
|
if (diagonalAllowed && openLeftDiagonal && TryMoveEmpty(x, y, x - 1, y + 1))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (lateralAllowed && openLeftLateral && TryMoveEmpty(x, y, x - 1, y))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (diagonalAllowed && openRightDiagonal && TryMoveEmpty(x, y, x + 1, y + 1))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (lateralAllowed && openRightLateral && TryMoveEmpty(x, y, x + 1, y))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (diagonalAllowed && openRightDiagonal && TryMoveEmpty(x, y, x + 1, y + 1))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (lateralAllowed && openRightLateral && TryMoveEmpty(x, y, x + 1, y))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (diagonalAllowed && openLeftDiagonal && TryMoveEmpty(x, y, x - 1, y + 1))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (lateralAllowed && openLeftLateral && TryMoveEmpty(x, y, x - 1, y))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (openLeftDiagonal || openRightDiagonal || openLeftLateral || openRightLateral)
|
|
{
|
|
RecordStalledMovable();
|
|
KeepChunkAwakeAt(x, y);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool TryMoveGas(int x, int y, PrototypeParticle particle, bool leftFirst, ref int seed, float netForceX, float netForceY)
|
|
{
|
|
var suppressRiseAndDiagonal = TrySkipGasRetry(x, y);
|
|
if (suppressRiseAndDiagonal && CanMoveIntoEmpty(x, y - 1))
|
|
{
|
|
ClearGasRetry(x, y);
|
|
suppressRiseAndDiagonal = false;
|
|
}
|
|
|
|
var openAbove = false;
|
|
if (!suppressRiseAndDiagonal)
|
|
{
|
|
openAbove = CanMoveIntoEmpty(x, y - 1);
|
|
if (openAbove)
|
|
{
|
|
ClearBlockedGasRise(x, y);
|
|
ClearGasRetry(x, y);
|
|
if (CanRiseGas(particle, ref seed, netForceY) && TryMoveEmpty(x, y, x, y - 1))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
else if (!TrySkipBlockedGasRise(x, y) && InBounds(x, y - 1))
|
|
{
|
|
CacheBlockedGasRise(x, y);
|
|
}
|
|
}
|
|
|
|
var windPreferred = netForceX > 0.15f ? 1 : netForceX < -0.15f ? -1 : 0;
|
|
if (windPreferred != 0 && CanDriftGas(particle, ref seed, netForceX, lateral: true) && TryMoveEmpty(x, y, x + windPreferred, y))
|
|
{
|
|
ClearGasRetry(x, y);
|
|
return true;
|
|
}
|
|
|
|
var diagonalAllowed = CanDriftGas(particle, ref seed, netForceX, lateral: false);
|
|
var lateralAllowed = diagonalAllowed || CanDriftGas(particle, ref seed, netForceX, lateral: true);
|
|
if (suppressRiseAndDiagonal)
|
|
{
|
|
diagonalAllowed = false;
|
|
}
|
|
|
|
var openLeftDiagonal = !suppressRiseAndDiagonal && CanMoveIntoEmpty(x - 1, y - 1);
|
|
var openRightDiagonal = !suppressRiseAndDiagonal && CanMoveIntoEmpty(x + 1, y - 1);
|
|
var openLeftLateral = CanMoveIntoEmpty(x - 1, y);
|
|
var openRightLateral = CanMoveIntoEmpty(x + 1, y);
|
|
if (leftFirst)
|
|
{
|
|
if (diagonalAllowed && openLeftDiagonal && TryMoveEmpty(x, y, x - 1, y - 1))
|
|
{
|
|
ClearGasRetry(x, y);
|
|
return true;
|
|
}
|
|
|
|
if (lateralAllowed && openLeftLateral && TryMoveEmpty(x, y, x - 1, y))
|
|
{
|
|
ClearGasRetry(x, y);
|
|
return true;
|
|
}
|
|
|
|
if (diagonalAllowed && openRightDiagonal && TryMoveEmpty(x, y, x + 1, y - 1))
|
|
{
|
|
ClearGasRetry(x, y);
|
|
return true;
|
|
}
|
|
|
|
if (lateralAllowed && openRightLateral && TryMoveEmpty(x, y, x + 1, y))
|
|
{
|
|
ClearGasRetry(x, y);
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (diagonalAllowed && openRightDiagonal && TryMoveEmpty(x, y, x + 1, y - 1))
|
|
{
|
|
ClearGasRetry(x, y);
|
|
return true;
|
|
}
|
|
|
|
if (lateralAllowed && openRightLateral && TryMoveEmpty(x, y, x + 1, y))
|
|
{
|
|
ClearGasRetry(x, y);
|
|
return true;
|
|
}
|
|
|
|
if (diagonalAllowed && openLeftDiagonal && TryMoveEmpty(x, y, x - 1, y - 1))
|
|
{
|
|
ClearGasRetry(x, y);
|
|
return true;
|
|
}
|
|
|
|
if (lateralAllowed && openLeftLateral && TryMoveEmpty(x, y, x - 1, y))
|
|
{
|
|
ClearGasRetry(x, y);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (!openAbove && !openLeftDiagonal && !openRightDiagonal)
|
|
{
|
|
CacheGasRetry(x, y, (openLeftLateral || openRightLateral) ? 1 : 2);
|
|
}
|
|
else
|
|
{
|
|
ClearGasRetry(x, y);
|
|
}
|
|
|
|
if (openLeftDiagonal || openRightDiagonal || openLeftLateral || openRightLateral)
|
|
{
|
|
RecordStalledMovable();
|
|
KeepChunkAwakeAt(x, y);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void MoveFire(int x, int y, bool leftFirst, ref int 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)
|
|
{
|
|
RemoveParticle(x, y);
|
|
}
|
|
}
|
|
|
|
private void MoveEmber(int x, int y, PrototypeParticle particle, bool leftFirst, ref int seed, float netForceX)
|
|
{
|
|
if (NextChance(ref seed) <= MathF.Max(0.1f, particle.UpwardBias))
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
TryMoveSolid(GetChunkCoord(x, y), GetLocalCoord(x, y).LocalX, GetLocalCoord(x, y).LocalY, x, y, particle, leftFirst, ref seed, netForceX);
|
|
}
|
|
|
|
private void MovePlasma(int x, int y, PrototypeParticle particle, bool leftFirst, ref int seed, float netForceX, float netForceY)
|
|
{
|
|
if (TryMoveEmpty(x, y, x, y - 1))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var lateralBias = MathF.Max(0.15f, particle.SideDriftBias);
|
|
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;
|
|
}
|
|
}
|
|
|
|
TryMoveGas(x, y, particle, leftFirst, ref seed, netForceX, netForceY);
|
|
}
|
|
|
|
private bool TryMoveSolidDown(int x, int y, PrototypeParticle particle)
|
|
{
|
|
if (TryMoveEmpty(x, y, x, y + 1))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!TryGetParticle(x, y + 1, out var target))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ((target.Kind is ParticleKind.Liquid or ParticleKind.Gas) && particle.Mass > target.Mass)
|
|
{
|
|
RecordSwapAttempt();
|
|
return SwapParticles(x, y, x, y + 1);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool TryMoveLiquidDown(int x, int y, PrototypeParticle particle)
|
|
{
|
|
if (TryMoveEmpty(x, y, x, y + 1))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!TryGetParticle(x, y + 1, out var target))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (target.Kind == ParticleKind.Gas && particle.Mass > target.Mass)
|
|
{
|
|
RecordSwapAttempt();
|
|
return SwapParticles(x, y, x, y + 1);
|
|
}
|
|
|
|
if (particle.IsMolten && target.Kind == ParticleKind.Liquid && particle.Mass > target.Mass)
|
|
{
|
|
RecordSwapAttempt();
|
|
return SwapParticles(x, y, x, y + 1);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool TryMoveEmpty(int fromX, int fromY, int toX, int toY)
|
|
{
|
|
RecordMoveAttempt(toX - fromX, toY - fromY);
|
|
if (!InBounds(toX, toY))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (TryGetCellPage(fromX, fromY, out var sourceCoord, out var sourcePage, out var sourceLocalX, out var sourceLocalY))
|
|
{
|
|
var destinationCoord = GetChunkCoord(toX, toY);
|
|
if (destinationCoord == sourceCoord)
|
|
{
|
|
var (destinationLocalX, destinationLocalY) = GetLocalCoord(toX, toY);
|
|
if (!sourcePage.IsOccupied(destinationLocalX, destinationLocalY))
|
|
{
|
|
return MoveParticleWithinPage(sourceCoord, sourcePage, sourceLocalX, sourceLocalY, destinationLocalX, destinationLocalY, fromX, fromY, toX, toY);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (!CanMoveIntoEmpty(toX, toY))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return MoveParticle(fromX, fromY, toX, toY);
|
|
}
|
|
|
|
private bool CanMoveIntoEmpty(int x, int y) => InBounds(x, y) && !HasParticle(x, y);
|
|
|
|
private int ResolveHorizontalPreference(ChunkCoord coord, int localX, int localY, PrototypeParticle particle, float netForceX)
|
|
{
|
|
if (netForceX > 0.15f)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
if (netForceX < -0.15f)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
var page = _cellPages[coord];
|
|
var driftState = page.GetDriftState(localX, localY);
|
|
if (driftState != 0)
|
|
{
|
|
return driftState;
|
|
}
|
|
|
|
return PreferLeft(unchecked((coord.X * 92821) + (coord.Y * 68917) + (localX * 31) + (localY * 131) + _stepCounter + particle.TypeId))
|
|
? -1
|
|
: 1;
|
|
}
|
|
|
|
private (float X, float Y) GetNetForce(int x, int y, PrototypeParticle particle)
|
|
{
|
|
if (!TryGetFieldPage(x, y, out _, out var page, out var localX, out var localY))
|
|
{
|
|
var gradientXOnly = SamplePressure(x - 1, y) - SamplePressure(x + 1, y);
|
|
var gradientYOnly = SamplePressure(x, y + 1) - SamplePressure(x, y - 1);
|
|
return (gradientXOnly * 0.12f * particle.PressureResponse, gradientYOnly * 0.08f * particle.PressureResponse);
|
|
}
|
|
|
|
var cell = page.GetCell(localX, localY);
|
|
var forceX = cell.WindX + cell.ForceX;
|
|
var forceY = cell.WindY + cell.ForceY;
|
|
var pressureGradientX = SamplePressure(x - 1, y) - SamplePressure(x + 1, y);
|
|
var pressureGradientY = SamplePressure(x, y + 1) - SamplePressure(x, y - 1);
|
|
forceX += pressureGradientX * 0.12f * particle.PressureResponse;
|
|
forceY += pressureGradientY * 0.08f * particle.PressureResponse;
|
|
if (particle.MotionType == PrototypeParticleType.Steam)
|
|
{
|
|
forceY -= 0.2f;
|
|
}
|
|
|
|
if (MathF.Abs(cell.Pressure) > 0.05f)
|
|
{
|
|
forceY += particle.MotionType == PrototypeParticleType.Steam
|
|
? -cell.Pressure * 0.05f * particle.PressureResponse
|
|
: cell.Pressure * 0.05f * particle.PressureResponse;
|
|
}
|
|
|
|
return (forceX, forceY);
|
|
}
|
|
|
|
private static float GetEffectiveVelocity(PrototypeParticle particle)
|
|
{
|
|
if (particle.Velocity > 0f)
|
|
{
|
|
return particle.Velocity;
|
|
}
|
|
|
|
return particle.Kind switch
|
|
{
|
|
ParticleKind.Solid => 0.45f,
|
|
ParticleKind.Liquid => 0.35f,
|
|
ParticleKind.Gas => 0.25f,
|
|
_ => 0.35f,
|
|
};
|
|
}
|
|
|
|
private static float GetForceResponse(PrototypeParticle particle)
|
|
{
|
|
var mobility = 0.35f + GetEffectiveVelocity(particle);
|
|
var damping = MathF.Max(0.25f, (particle.Mass * 0.85f) + (particle.Friction * 0.35f) + (particle.Viscosity * 0.45f));
|
|
return Math.Clamp((mobility / damping) * particle.ForceResponseMultiplier, 0.15f, 2f);
|
|
}
|
|
|
|
private static bool CanSlipSolid(PrototypeParticle particle, ref int seed, float netForceX)
|
|
{
|
|
if (MathF.Abs(netForceX) > 0.45f)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var chance = Math.Clamp(0.2f + (GetEffectiveVelocity(particle) * 0.95f) - (particle.Friction * 0.35f), 0.1f, 0.95f);
|
|
return NextChance(ref seed) <= chance;
|
|
}
|
|
|
|
private static bool CanFlowLiquid(PrototypeParticle particle, ref int seed, bool lateral, float netForceX)
|
|
{
|
|
var forceBonus = MathF.Min(0.25f, MathF.Abs(netForceX) * 0.08f);
|
|
var velocity = GetEffectiveVelocity(particle);
|
|
var baseChance = lateral
|
|
? 0.18f + (velocity * 0.95f) - (particle.Viscosity * 0.42f) - (particle.Friction * 0.08f)
|
|
: 0.28f + (velocity * 0.9f) - (particle.Viscosity * 0.28f) - (particle.Friction * 0.05f);
|
|
var flowScale = lateral ? particle.LateralFlowMultiplier : particle.DiagonalFlowMultiplier;
|
|
var chance = Math.Clamp((baseChance * flowScale) + forceBonus, 0.03f, 0.98f);
|
|
return NextChance(ref seed) <= chance;
|
|
}
|
|
|
|
private static bool CanRiseGas(PrototypeParticle particle, ref int seed, float netForceY)
|
|
{
|
|
if (netForceY < -0.65f)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var velocity = GetEffectiveVelocity(particle);
|
|
var buoyancy = MathF.Max(0f, -netForceY) * 0.08f;
|
|
var chance = Math.Clamp(0.1f + (velocity * 1.8f) - (particle.Viscosity * 0.22f) + buoyancy, 0.05f, 0.99f);
|
|
return NextChance(ref seed) <= chance;
|
|
}
|
|
|
|
private static bool CanDriftGas(PrototypeParticle particle, ref int seed, float netForceX, bool lateral)
|
|
{
|
|
var forceBonus = MathF.Min(0.28f, MathF.Abs(netForceX) * 0.08f);
|
|
var velocity = GetEffectiveVelocity(particle);
|
|
var baseChance = lateral
|
|
? 0.18f + (velocity * 1.15f) - (particle.Viscosity * 0.1f)
|
|
: 0.22f + (velocity * 1.2f) - (particle.Viscosity * 0.16f);
|
|
var chance = Math.Clamp(baseChance + forceBonus - (particle.Friction * 0.04f), 0.05f, 0.99f);
|
|
return NextChance(ref seed) <= chance;
|
|
}
|
|
|
|
private static sbyte GetDefaultDriftDirection(int x, int y) => PreferLeft(unchecked((x * 73856093) ^ (y * 19349663))) ? (sbyte)-1 : (sbyte)1;
|
|
|
|
private bool TrySkipBlockedSolid(ChunkCoord coord, int localX, int localY, float netForceX)
|
|
{
|
|
if (MathF.Abs(netForceX) > 0.1f ||
|
|
localX <= 0 ||
|
|
localX >= _config.ChunkWidth - 1 ||
|
|
localY >= _config.ChunkHeight - 1 ||
|
|
!_cellPages.TryGetValue(coord, out var page))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var signature = ComputeSolidBlockSignature(page, localY);
|
|
return signature != 0 && page.GetBlockedSolidSignature(localX, localY) == signature;
|
|
}
|
|
|
|
private void CacheBlockedSolid(ChunkCoord coord, int localX, int localY)
|
|
{
|
|
if (localX <= 0 || localX >= _config.ChunkWidth - 1 || localY >= _config.ChunkHeight - 1)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_cellPages.TryGetValue(coord, out var page))
|
|
{
|
|
page.SetBlockedSolidSignature(localX, localY, ComputeSolidBlockSignature(page, localY));
|
|
}
|
|
}
|
|
|
|
private static int ComputeSolidBlockSignature(ChunkCellPage page, int localY)
|
|
{
|
|
if (localY < 0 || localY >= page.Height - 1)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
return HashCode.Combine(page.GetRowRevision(localY), page.GetRowRevision(localY + 1));
|
|
}
|
|
|
|
private bool CanThrottleGasRuntime(int x, int y, PrototypeParticle particle, float temperature)
|
|
{
|
|
if (particle.Kind != ParticleKind.Gas ||
|
|
particle.HasLifetimeBehavior ||
|
|
particle.HasBurnBehavior ||
|
|
particle.HasEmissionBehavior ||
|
|
particle.HasSpecialBehavior ||
|
|
particle.HasReactionBehavior)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var requiresPressureSensitiveRuntime = particle.HasPressureBehavior || particle.HasPhaseBehavior;
|
|
if (requiresPressureSensitiveRuntime &&
|
|
(GetPressureLoad(x, y) > 0.2f || GetPressureDurationAt(x, y) > 0f))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (MathF.Abs(temperature - _ambientTemperature) <= 0.5f)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (HasThermallyActiveNeighbor(x, y, temperature, out var hasPassiveBoundaryNeighbor))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (hasPassiveBoundaryNeighbor)
|
|
{
|
|
return ((_stepCounter + (x * 3) + (y * 5)) & 3) != 0;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool HasThermallyActiveNeighbor(int x, int y, float localTemperature, out bool hasPassiveBoundaryNeighbor)
|
|
{
|
|
hasPassiveBoundaryNeighbor = false;
|
|
Span<(int X, int Y)> neighbors = [(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)];
|
|
for (var i = 0; i < neighbors.Length; i++)
|
|
{
|
|
var (nx, ny) = neighbors[i];
|
|
if (!TryGetParticle(nx, ny, out var neighbor))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var neighborTemperature = GetTemperatureAt(nx, ny);
|
|
if (MathF.Abs(neighborTemperature - localTemperature) > 8f)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (neighbor.HasBurnBehavior ||
|
|
neighbor.HasEmissionBehavior ||
|
|
neighbor.HasSpecialBehavior ||
|
|
neighbor.HasFlag(PrototypeParticleFlags.HotSource))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (neighbor.HasReactionBehavior || neighbor.HasPhaseBehavior || neighbor.HasPressureBehavior)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (neighbor.Kind == ParticleKind.Gas)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (neighbor.IsStatic || neighbor.MotionType == PrototypeParticleType.Wall)
|
|
{
|
|
hasPassiveBoundaryNeighbor = true;
|
|
continue;
|
|
}
|
|
|
|
if (neighbor.Conductivity > 0.25f || MathF.Abs(neighborTemperature - localTemperature) > 24f)
|
|
{
|
|
hasPassiveBoundaryNeighbor = true;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool TrySkipBlockedGasRise(int x, int y)
|
|
{
|
|
if (!TryGetCellPage(x, y, out _, out var page, out var localX, out var localY) || localY <= 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!page.IsOccupied(localX, localY - 1))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var signature = ComputeGasRiseBlockSignature(page, localX, localY);
|
|
return signature != 0 && page.GetBlockedGasRiseSignature(localX, localY) == signature;
|
|
}
|
|
|
|
private void CacheBlockedGasRise(int x, int y)
|
|
{
|
|
if (!TryGetCellPage(x, y, out _, out var page, out var localX, out var localY) || localY <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!page.IsOccupied(localX, localY - 1))
|
|
{
|
|
page.SetBlockedGasRiseSignature(localX, localY, 0);
|
|
return;
|
|
}
|
|
|
|
page.SetBlockedGasRiseSignature(localX, localY, ComputeGasRiseBlockSignature(page, localX, localY));
|
|
}
|
|
|
|
private void ClearBlockedGasRise(int x, int y)
|
|
{
|
|
if (!TryGetCellPage(x, y, out _, out var page, out var localX, out var localY))
|
|
{
|
|
return;
|
|
}
|
|
|
|
page.SetBlockedGasRiseSignature(localX, localY, 0);
|
|
}
|
|
|
|
private static int ComputeGasRiseBlockSignature(ChunkCellPage page, int localX, int localY)
|
|
{
|
|
if (localY <= 0 || localY >= page.Height)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
return HashCode.Combine(localX, page.GetRowRevision(localY), page.GetRowRevision(localY - 1));
|
|
}
|
|
|
|
private bool TrySkipGasRetry(int x, int y)
|
|
{
|
|
if (!TryGetCellPage(x, y, out _, out var page, out var localX, out var localY))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var signature = ComputeGasRetrySignature(page, localX, localY);
|
|
if (signature == 0)
|
|
{
|
|
page.ClearGasRetryState(localX, localY);
|
|
return false;
|
|
}
|
|
|
|
if (page.GetGasRetryUntilStep(localX, localY) > _stepCounter && page.GetGasRetrySignature(localX, localY) == signature)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (page.GetGasRetrySignature(localX, localY) != signature)
|
|
{
|
|
page.ClearGasRetryState(localX, localY);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void CacheGasRetry(int x, int y, int cooldownSteps)
|
|
{
|
|
if (!TryGetCellPage(x, y, out _, out var page, out var localX, out var localY))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var signature = ComputeGasRetrySignature(page, localX, localY);
|
|
if (signature == 0)
|
|
{
|
|
page.ClearGasRetryState(localX, localY);
|
|
return;
|
|
}
|
|
|
|
page.SetGasRetryState(localX, localY, signature, _stepCounter + Math.Max(1, cooldownSteps));
|
|
}
|
|
|
|
private void ClearGasRetry(int x, int y)
|
|
{
|
|
if (!TryGetCellPage(x, y, out _, out var page, out var localX, out var localY))
|
|
{
|
|
return;
|
|
}
|
|
|
|
page.ClearGasRetryState(localX, localY);
|
|
}
|
|
|
|
private static int ComputeGasRetrySignature(ChunkCellPage page, int localX, int localY)
|
|
{
|
|
if (localY <= 0 || localY >= page.Height)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
return HashCode.Combine(
|
|
localX,
|
|
page.GetRowRevision(localY),
|
|
page.GetRowRevision(localY - 1),
|
|
page.GetOccupiedRowCount(localY),
|
|
page.GetOccupiedRowCount(localY - 1));
|
|
}
|
|
|
|
private static bool PreferLeft(int seed) => ((seed ^ 0x5bd1e995) & 1) == 0;
|
|
|
|
private static float NextChance(ref int seed)
|
|
{
|
|
seed = unchecked((seed * 1103515245) + 12345);
|
|
return ((seed >> 8) & 0x00FFFFFF) / 16777215f;
|
|
}
|
|
|
|
private static sbyte ResolveDriftAfterMove(int fromX, int toX, sbyte currentDrift)
|
|
{
|
|
if (toX > fromX)
|
|
{
|
|
return 1;
|
|
}
|
|
|
|
if (toX < fromX)
|
|
{
|
|
return -1;
|
|
}
|
|
|
|
return currentDrift;
|
|
}
|
|
|
|
private static PrototypeParticle CreateLegacyParticle(PrototypeParticleType type) => type switch
|
|
{
|
|
PrototypeParticleType.Sand => new PrototypeParticle(1, "sand", type, ParticleKind.Solid, ParticleBehaviorKind.None, 214, 188, 96, 1.4f, 0.65f, 0.18f, 0.08f),
|
|
PrototypeParticleType.Water => new PrototypeParticle(2, "water", type, ParticleKind.Liquid, ParticleBehaviorKind.None, 72, 132, 232, 1.0f, 0.55f, 0.04f, 0.12f, Flags: PrototypeParticleFlags.WaterLike),
|
|
PrototypeParticleType.Steam => new PrototypeParticle(3, "steam", type, ParticleKind.Gas, ParticleBehaviorKind.None, 182, 196, 214, 0.2f, 0.7f, 0.01f, 0.03f, SolidifyTypeId: 2, PressureThreshold: 1.2f, InitialTemperature: 110f),
|
|
PrototypeParticleType.Wall => new PrototypeParticle(4, "wall", type, ParticleKind.Solid, ParticleBehaviorKind.None, 96, 96, 104, 100f, 0f, 1f, 1f, IsStatic: true),
|
|
_ => default,
|
|
};
|
|
|
|
private void KeepChunkAwakeAt(int x, int y)
|
|
{
|
|
if (!TryGetCellPage(x, y, out _, out var page, out var localX, out var localY))
|
|
{
|
|
return;
|
|
}
|
|
|
|
page.IsActive = true;
|
|
page.LastTouchedFrame = _stepCounter;
|
|
page.RequestFutureSteps(2);
|
|
page.MarkDirtyBand(localX, localY);
|
|
}
|
|
}
|