800 lines
23 KiB
C#
800 lines
23 KiB
C#
namespace Sand.Core;
|
|
|
|
public sealed partial class SandSimulation
|
|
{
|
|
private uint GetVisualSettingsStamp()
|
|
{
|
|
unchecked
|
|
{
|
|
var stamp = 17u;
|
|
stamp = (stamp * 31u) + (_settings.EnablePressureVisuals ? 1u : 0u);
|
|
stamp = (stamp * 31u) + (_settings.EnableWindVisuals ? 1u : 0u);
|
|
stamp = (stamp * 31u) + (_settings.EnableTempVisuals ? 1u : 0u);
|
|
stamp = (stamp * 31u) + (_settings.EnableGasEffect ? 1u : 0u);
|
|
stamp = (stamp * 31u) + (_settings.EnableGlow ? 1u : 0u);
|
|
stamp = (stamp * 31u) + (uint)BitConverter.SingleToInt32Bits(_settings.AmbientTemperature);
|
|
return stamp;
|
|
}
|
|
}
|
|
|
|
private void EnsureVisualSettingsState()
|
|
{
|
|
var stamp = GetVisualSettingsStamp();
|
|
if (_visualSettingsStamp == stamp)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_visualSettingsStamp = stamp;
|
|
MarkFullVisualDirty();
|
|
}
|
|
|
|
private void MarkFullVisualDirty()
|
|
{
|
|
_fullVisualDirty = true;
|
|
_hasDirtyVisualBounds = true;
|
|
_minDirtyVisualX = 0;
|
|
_minDirtyVisualY = 0;
|
|
_maxDirtyVisualX = Width - 1;
|
|
_maxDirtyVisualY = Height - 1;
|
|
}
|
|
|
|
private bool AreFieldVisualsEnabled() => _settings.EnablePressureVisuals || _settings.EnableWindVisuals;
|
|
|
|
private void MarkVisualDirty(int x, int y)
|
|
{
|
|
if (!InBounds(x, y))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_deferVisualDirtyTracking)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_fullVisualDirty)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!_hasDirtyVisualBounds)
|
|
{
|
|
_minDirtyVisualX = _maxDirtyVisualX = x;
|
|
_minDirtyVisualY = _maxDirtyVisualY = y;
|
|
_hasDirtyVisualBounds = true;
|
|
return;
|
|
}
|
|
|
|
_minDirtyVisualX = Math.Min(_minDirtyVisualX, x);
|
|
_minDirtyVisualY = Math.Min(_minDirtyVisualY, y);
|
|
_maxDirtyVisualX = Math.Max(_maxDirtyVisualX, x);
|
|
_maxDirtyVisualY = Math.Max(_maxDirtyVisualY, y);
|
|
}
|
|
|
|
private void MarkVisualDirtyRect(int startX, int startY, int endX, int endY)
|
|
{
|
|
if (_deferVisualDirtyTracking)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_fullVisualDirty)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (startX > endX || startY > endY)
|
|
{
|
|
return;
|
|
}
|
|
|
|
startX = Math.Max(0, startX);
|
|
startY = Math.Max(0, startY);
|
|
endX = Math.Min(Width - 1, endX);
|
|
endY = Math.Min(Height - 1, endY);
|
|
if (startX > endX || startY > endY)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!_hasDirtyVisualBounds)
|
|
{
|
|
_minDirtyVisualX = startX;
|
|
_minDirtyVisualY = startY;
|
|
_maxDirtyVisualX = endX;
|
|
_maxDirtyVisualY = endY;
|
|
_hasDirtyVisualBounds = true;
|
|
return;
|
|
}
|
|
|
|
_minDirtyVisualX = Math.Min(_minDirtyVisualX, startX);
|
|
_minDirtyVisualY = Math.Min(_minDirtyVisualY, startY);
|
|
_maxDirtyVisualX = Math.Max(_maxDirtyVisualX, endX);
|
|
_maxDirtyVisualY = Math.Max(_maxDirtyVisualY, endY);
|
|
}
|
|
|
|
private bool TryGetDirtyVisualBounds(out int startX, out int startY, out int endX, out int endY)
|
|
{
|
|
if (_fullVisualDirty)
|
|
{
|
|
startX = 0;
|
|
startY = 0;
|
|
endX = Width - 1;
|
|
endY = Height - 1;
|
|
return true;
|
|
}
|
|
|
|
if (_hasDirtyVisualBounds)
|
|
{
|
|
startX = _minDirtyVisualX;
|
|
startY = _minDirtyVisualY;
|
|
endX = _maxDirtyVisualX;
|
|
endY = _maxDirtyVisualY;
|
|
return true;
|
|
}
|
|
|
|
startX = startY = endX = endY = 0;
|
|
return false;
|
|
}
|
|
|
|
private void ClearDirtyVisualBounds()
|
|
{
|
|
_fullVisualDirty = false;
|
|
_hasDirtyVisualBounds = false;
|
|
}
|
|
|
|
private void UpdateCachedVisualBuffers()
|
|
{
|
|
EnsureVisualSettingsState();
|
|
if (!TryGetDirtyVisualBounds(out var startX, out var startY, out var endX, out var endY))
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var y = startY; y <= endY; y++)
|
|
{
|
|
for (var x = startX; x <= endX; x++)
|
|
{
|
|
var color = ResolveVisualColor(x, y);
|
|
var pixelIndex = (y * Width) + x;
|
|
var rgbIndex = pixelIndex * 3;
|
|
var rgbaIndex = pixelIndex * 4;
|
|
_rgbBuffer[rgbIndex] = color.R;
|
|
_rgbBuffer[rgbIndex + 1] = color.G;
|
|
_rgbBuffer[rgbIndex + 2] = color.B;
|
|
_rgbaBuffer[rgbaIndex] = color.R;
|
|
_rgbaBuffer[rgbaIndex + 1] = color.G;
|
|
_rgbaBuffer[rgbaIndex + 2] = color.B;
|
|
_rgbaBuffer[rgbaIndex + 3] = 255;
|
|
}
|
|
}
|
|
|
|
ClearDirtyVisualBounds();
|
|
}
|
|
|
|
private bool HasDynamicFieldAt(int x, int y)
|
|
{
|
|
return
|
|
MathF.Abs(_windFieldX[x, y]) >= 0.02f ||
|
|
MathF.Abs(_windFieldY[x, y]) >= 0.02f ||
|
|
MathF.Abs(_forceFieldX[x, y]) >= 0.02f ||
|
|
MathF.Abs(_forceFieldY[x, y]) >= 0.02f ||
|
|
MathF.Abs(_airPressure[x, y]) >= 0.02f;
|
|
}
|
|
|
|
private void ExpandActiveFieldBoundsToInclude(int x, int y)
|
|
{
|
|
if (!_hasActiveFieldBounds)
|
|
{
|
|
_minActiveFieldX = _maxActiveFieldX = x;
|
|
_minActiveFieldY = _maxActiveFieldY = y;
|
|
_hasActiveFieldBounds = true;
|
|
return;
|
|
}
|
|
|
|
_minActiveFieldX = Math.Min(_minActiveFieldX, x);
|
|
_minActiveFieldY = Math.Min(_minActiveFieldY, y);
|
|
_maxActiveFieldX = Math.Max(_maxActiveFieldX, x);
|
|
_maxActiveFieldY = Math.Max(_maxActiveFieldY, y);
|
|
}
|
|
|
|
private void TrackDynamicFieldMutation(int x, int y, bool hadDynamicBefore)
|
|
{
|
|
if (!InBounds(x, y))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var hasDynamicAfter = HasDynamicFieldAt(x, y);
|
|
if (hasDynamicAfter)
|
|
{
|
|
ExpandActiveFieldBoundsToInclude(x, y);
|
|
}
|
|
else if (hadDynamicBefore)
|
|
{
|
|
_fieldBoundsDirty = true;
|
|
}
|
|
|
|
if (AreFieldVisualsEnabled())
|
|
{
|
|
MarkVisualDirty(x, y);
|
|
}
|
|
}
|
|
|
|
private void ClearDynamicFieldsAt(int x, int y)
|
|
{
|
|
var hadDynamicBefore = HasDynamicFieldAt(x, y);
|
|
_windFieldX[x, y] = 0f;
|
|
_windFieldY[x, y] = 0f;
|
|
_forceFieldX[x, y] = 0f;
|
|
_forceFieldY[x, y] = 0f;
|
|
_airPressure[x, y] = 0f;
|
|
if (hadDynamicBefore)
|
|
{
|
|
_fieldBoundsDirty = true;
|
|
if (AreFieldVisualsEnabled())
|
|
{
|
|
MarkVisualDirty(x, y);
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool TryGetOccupiedBoundsOnly(out int startX, out int startY, out int endX, out int endY)
|
|
{
|
|
if (_hasOccupiedBounds)
|
|
{
|
|
startX = _minOccupiedX;
|
|
startY = _minOccupiedY;
|
|
endX = _maxOccupiedX;
|
|
endY = _maxOccupiedY;
|
|
return true;
|
|
}
|
|
|
|
startX = startY = endX = endY = 0;
|
|
return false;
|
|
}
|
|
|
|
private bool TryGetSimulationBounds(out int startX, out int startY, out int endX, out int endY)
|
|
{
|
|
EnsureOccupiedBounds();
|
|
if (_fieldBoundsDirty && (_hasActiveFieldBounds || _hasOccupiedBounds))
|
|
{
|
|
var recomputeStartX = _hasActiveFieldBounds && _hasOccupiedBounds ? Math.Min(_minActiveFieldX, _minOccupiedX) : _hasActiveFieldBounds ? _minActiveFieldX : _minOccupiedX;
|
|
var recomputeStartY = _hasActiveFieldBounds && _hasOccupiedBounds ? Math.Min(_minActiveFieldY, _minOccupiedY) : _hasActiveFieldBounds ? _minActiveFieldY : _minOccupiedY;
|
|
var recomputeEndX = _hasActiveFieldBounds && _hasOccupiedBounds ? Math.Max(_maxActiveFieldX, _maxOccupiedX) : _hasActiveFieldBounds ? _maxActiveFieldX : _maxOccupiedX;
|
|
var recomputeEndY = _hasActiveFieldBounds && _hasOccupiedBounds ? Math.Max(_maxActiveFieldY, _maxOccupiedY) : _hasActiveFieldBounds ? _maxActiveFieldY : _maxOccupiedY;
|
|
RebuildActiveFieldBounds(recomputeStartX, recomputeStartY, recomputeEndX, recomputeEndY);
|
|
}
|
|
|
|
var hasBounds = false;
|
|
startX = startY = endX = endY = 0;
|
|
if (_hasOccupiedBounds)
|
|
{
|
|
startX = _minOccupiedX;
|
|
startY = _minOccupiedY;
|
|
endX = _maxOccupiedX;
|
|
endY = _maxOccupiedY;
|
|
hasBounds = true;
|
|
}
|
|
|
|
if (_hasActiveFieldBounds)
|
|
{
|
|
if (!hasBounds)
|
|
{
|
|
startX = _minActiveFieldX;
|
|
startY = _minActiveFieldY;
|
|
endX = _maxActiveFieldX;
|
|
endY = _maxActiveFieldY;
|
|
hasBounds = true;
|
|
}
|
|
else
|
|
{
|
|
startX = Math.Min(startX, _minActiveFieldX);
|
|
startY = Math.Min(startY, _minActiveFieldY);
|
|
endX = Math.Max(endX, _maxActiveFieldX);
|
|
endY = Math.Max(endY, _maxActiveFieldY);
|
|
}
|
|
}
|
|
|
|
return hasBounds;
|
|
}
|
|
|
|
private void MarkBoundsUnionDirty(bool hadFirstBounds, int firstStartX, int firstStartY, int firstEndX, int firstEndY)
|
|
{
|
|
int secondStartX;
|
|
int secondStartY;
|
|
int secondEndX;
|
|
int secondEndY;
|
|
var hasSecondBounds = AreFieldVisualsEnabled()
|
|
? TryGetSimulationBounds(out secondStartX, out secondStartY, out secondEndX, out secondEndY)
|
|
: TryGetOccupiedBoundsOnly(out secondStartX, out secondStartY, out secondEndX, out secondEndY);
|
|
|
|
if (hasSecondBounds)
|
|
{
|
|
if (!hadFirstBounds)
|
|
{
|
|
MarkVisualDirtyRect(secondStartX, secondStartY, secondEndX, secondEndY);
|
|
return;
|
|
}
|
|
|
|
MarkVisualDirtyRect(
|
|
Math.Min(firstStartX, secondStartX),
|
|
Math.Min(firstStartY, secondStartY),
|
|
Math.Max(firstEndX, secondEndX),
|
|
Math.Max(firstEndY, secondEndY));
|
|
return;
|
|
}
|
|
|
|
if (hadFirstBounds)
|
|
{
|
|
MarkVisualDirtyRect(firstStartX, firstStartY, firstEndX, firstEndY);
|
|
}
|
|
}
|
|
|
|
private ushort ResolveOptionalTypeId(string? particleId)
|
|
{
|
|
if (particleId is null)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
return _library.TryGetTypeId(particleId, out var typeId) ? typeId : (ushort)0;
|
|
}
|
|
|
|
private float ResolveDefaultLifetime(ParticleDef definition, ParticleBalanceProfile balance)
|
|
{
|
|
var lifetime = (definition.Lifetime ?? 0f) * MathF.Max(0.01f, balance.LifetimeMultiplier);
|
|
if (lifetime <= 0f && balance.MaxLifetimeTicks > 0f)
|
|
{
|
|
lifetime = (balance.MinLifetimeTicks + balance.MaxLifetimeTicks) * 0.5f;
|
|
}
|
|
|
|
if (balance.MinLifetimeTicks > 0f)
|
|
{
|
|
lifetime = MathF.Max(lifetime, balance.MinLifetimeTicks);
|
|
}
|
|
|
|
if (balance.MaxLifetimeTicks > 0f)
|
|
{
|
|
lifetime = MathF.Min(lifetime, balance.MaxLifetimeTicks);
|
|
}
|
|
|
|
return lifetime;
|
|
}
|
|
|
|
private ToolProfile BuildToolProfile(string id, float defaultStrength, int fallbackRadiusCells)
|
|
{
|
|
if (_library.TryGetTypeId(id, out var typeId))
|
|
{
|
|
var definition = _library.GetDefinition(typeId);
|
|
var radiusCells = definition.Radius > 0f ? Math.Max(fallbackRadiusCells, (int)MathF.Ceiling(definition.Radius / (ParticleSize * 4f))) : fallbackRadiusCells;
|
|
var strength = definition.WindStrength > 0f ? definition.WindStrength : definition.GravityStrength > 0f ? definition.GravityStrength : definition.RepulsionStrength > 0f ? definition.RepulsionStrength : defaultStrength;
|
|
return new ToolProfile(
|
|
definition.Id,
|
|
radiusCells,
|
|
strength,
|
|
definition.ForceFalloff <= 0f ? 1f : definition.ForceFalloff,
|
|
definition.Turbulence,
|
|
definition.Affects.Length == 0 ? ["all"] : definition.Affects);
|
|
}
|
|
|
|
return new ToolProfile(id, fallbackRadiusCells, defaultStrength, 1f, 0f, ["all"]);
|
|
}
|
|
|
|
private void ApplyDirectionalBrush(int centerX, int centerY, int brushRadius, float forceX, float forceY, ToolProfile profile, float[,] fieldX, float[,] fieldY, bool addPressure = false)
|
|
{
|
|
var gridCenterX = centerX / ParticleSize;
|
|
var gridCenterY = centerY / ParticleSize;
|
|
var effectiveRadius = Math.Max(brushRadius, profile.RadiusCells);
|
|
var magnitude = MathF.Sqrt((forceX * forceX) + (forceY * forceY));
|
|
if (magnitude <= 0.001f)
|
|
{
|
|
forceX = 1f;
|
|
forceY = 0f;
|
|
magnitude = 1f;
|
|
}
|
|
|
|
for (var dx = -effectiveRadius; dx <= effectiveRadius; dx++)
|
|
{
|
|
for (var dy = -effectiveRadius; dy <= effectiveRadius; dy++)
|
|
{
|
|
if ((dx * dx) + (dy * dy) > 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;
|
|
}
|
|
|
|
if (!ToolAffectsCell(profile.Affects, x, y))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var distance = MathF.Sqrt((dx * dx) + (dy * dy));
|
|
var falloff = ComputeFalloff(distance, effectiveRadius, profile.Falloff);
|
|
var turbulence = SampleTurbulence(x, y, profile.Turbulence);
|
|
var hadDynamicBefore = HasDynamicFieldAt(x, y);
|
|
fieldX[x, y] = Math.Clamp(fieldX[x, y] + (((forceX / magnitude) + turbulence.X) * falloff * profile.Strength), -8f, 8f);
|
|
fieldY[x, y] = Math.Clamp(fieldY[x, y] + (((forceY / magnitude) + turbulence.Y) * falloff * profile.Strength), -8f, 8f);
|
|
if (addPressure)
|
|
{
|
|
_airPressure[x, y] = Math.Clamp(_airPressure[x, y] + falloff * profile.Strength, -10f, 10f);
|
|
}
|
|
TrackDynamicFieldMutation(x, y, hadDynamicBefore);
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool ToolAffectsCell(string[] affects, int x, int y)
|
|
{
|
|
if (affects.Length == 0 || affects.Contains("all", StringComparer.OrdinalIgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var typeId = TypeId[x, y];
|
|
if (typeId == 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var definition = _library.GetDefinition(typeId);
|
|
foreach (var affect in affects)
|
|
{
|
|
if (string.Equals(affect, definition.Id, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (affect == "solid" && definition.Kind == ParticleKind.Solid)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (affect == "liquid" && definition.Kind == ParticleKind.Liquid)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (affect == "gas" && definition.Kind == ParticleKind.Gas)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static float ComputeFalloff(float distance, int radius, float exponent)
|
|
{
|
|
if (radius <= 0)
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
var normalized = 1f - (distance / radius);
|
|
if (normalized <= 0f)
|
|
{
|
|
return 0f;
|
|
}
|
|
|
|
return MathF.Pow(normalized, MathF.Max(0.1f, exponent));
|
|
}
|
|
|
|
private (float X, float Y) SampleTurbulence(int x, int y, float turbulence)
|
|
{
|
|
if (turbulence <= 0.001f)
|
|
{
|
|
return (0f, 0f);
|
|
}
|
|
|
|
var hash = Hash(x, y, Frame);
|
|
var tx = (((hash & 0xFFu) / 255f) - 0.5f) * turbulence;
|
|
var ty = ((((hash >> 8) & 0xFFu) / 255f) - 0.5f) * turbulence;
|
|
return (tx, ty);
|
|
}
|
|
|
|
private void TrySpawnOnDeath(int x, int y, ushort typeId)
|
|
{
|
|
var spawnType = _producesOnDeathTarget[typeId];
|
|
if (spawnType == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
SpawnNeighborParticle(x, y, spawnType, Hash(x, y, Frame + 17));
|
|
}
|
|
|
|
private bool SpawnNeighborParticle(int x, int y, ushort spawnType, uint seed)
|
|
{
|
|
var offsets = new (int X, int Y)[]
|
|
{
|
|
(0, -1),
|
|
(-1, 0),
|
|
(1, 0),
|
|
(0, 1),
|
|
(-1, -1),
|
|
(1, -1),
|
|
(-1, 1),
|
|
(1, 1),
|
|
};
|
|
|
|
var start = (int)(seed % (uint)offsets.Length);
|
|
for (var i = 0; i < offsets.Length; i++)
|
|
{
|
|
var offset = offsets[(start + i) % offsets.Length];
|
|
var nx = x + offset.X;
|
|
var ny = y + offset.Y;
|
|
if (!TryNormalizeCoordinate(ref nx, ref ny))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if ((_settings.OuterWall && IsBoundary(nx, ny)) || TypeId[nx, ny] != 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
SetCell(nx, ny, spawnType);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool BreakCellFromPressure(int x, int y, ushort typeId, ref uint seed)
|
|
{
|
|
var brokenType = _brokenTarget[typeId];
|
|
if (brokenType != 0 && brokenType != typeId)
|
|
{
|
|
ReplaceCell(x, y, brokenType);
|
|
Temperature[x, y] = MathF.Max(Temperature[x, y], _initialTemperature[brokenType]);
|
|
return true;
|
|
}
|
|
|
|
var isHotBreak = Temperature[x, y] >= Math.Max(200f, _burnTemperature[typeId]);
|
|
if (_idSmoke != 0 && isHotBreak && NextChance(ref seed, 0.08f))
|
|
{
|
|
SpawnNeighborParticle(x, y, _idSmoke, seed);
|
|
}
|
|
|
|
ResetCell(x, y);
|
|
return true;
|
|
}
|
|
|
|
private void EnsureOccupiedBounds()
|
|
{
|
|
if (_boundsDirty && _staleOccupiedBoundsFrames >= 8)
|
|
{
|
|
RecomputeOccupiedBounds();
|
|
}
|
|
}
|
|
|
|
private void MarkBoundsDirtyIfRemovingOccupiedCell(int x, int y)
|
|
{
|
|
if (ParticleCount == 0)
|
|
{
|
|
_hasOccupiedBounds = false;
|
|
_boundsDirty = false;
|
|
return;
|
|
}
|
|
|
|
if (!_hasOccupiedBounds)
|
|
{
|
|
_boundsDirty = true;
|
|
return;
|
|
}
|
|
|
|
if (x == _minOccupiedX || x == _maxOccupiedX || y == _minOccupiedY || y == _maxOccupiedY)
|
|
{
|
|
_boundsDirty = true;
|
|
}
|
|
}
|
|
|
|
private void ExpandOccupiedBoundsToInclude(int x, int y)
|
|
{
|
|
if (!_hasOccupiedBounds)
|
|
{
|
|
_minOccupiedX = _maxOccupiedX = x;
|
|
_minOccupiedY = _maxOccupiedY = y;
|
|
_hasOccupiedBounds = true;
|
|
return;
|
|
}
|
|
|
|
_minOccupiedX = Math.Min(_minOccupiedX, x);
|
|
_minOccupiedY = Math.Min(_minOccupiedY, y);
|
|
_maxOccupiedX = Math.Max(_maxOccupiedX, x);
|
|
_maxOccupiedY = Math.Max(_maxOccupiedY, y);
|
|
}
|
|
|
|
private void RecomputeOccupiedBounds()
|
|
{
|
|
_hasOccupiedBounds = false;
|
|
for (var y = 0; y < Height; y++)
|
|
{
|
|
for (var x = 0; x < Width; x++)
|
|
{
|
|
if (TypeId[x, y] != 0)
|
|
{
|
|
ExpandOccupiedBoundsToInclude(x, y);
|
|
}
|
|
}
|
|
}
|
|
|
|
_boundsDirty = false;
|
|
_staleOccupiedBoundsFrames = 0;
|
|
}
|
|
|
|
private void ClearBoundaryWalls()
|
|
{
|
|
for (var x = 0; x < Width; x++)
|
|
{
|
|
TryClearBoundaryWall(x, 0);
|
|
TryClearBoundaryWall(x, Height - 1);
|
|
}
|
|
|
|
for (var y = 0; y < Height; y++)
|
|
{
|
|
TryClearBoundaryWall(0, y);
|
|
TryClearBoundaryWall(Width - 1, y);
|
|
}
|
|
}
|
|
|
|
private void TryClearBoundaryWall(int x, int y)
|
|
{
|
|
if (TypeId[x, y] == _idWall)
|
|
{
|
|
ParticleCount = Math.Max(0, ParticleCount - 1);
|
|
MarkBoundsDirtyIfRemovingOccupiedCell(x, y);
|
|
ResetCellWithoutCountChange(x, y);
|
|
MarkVisualDirty(x, y);
|
|
}
|
|
}
|
|
|
|
private void ApplyBoundaryWallsFast()
|
|
{
|
|
for (var x = 0; x < Width; x++)
|
|
{
|
|
SetBoundaryWall(x, 0);
|
|
SetBoundaryWall(x, Height - 1);
|
|
}
|
|
|
|
for (var y = 0; y < Height; y++)
|
|
{
|
|
SetBoundaryWall(0, y);
|
|
SetBoundaryWall(Width - 1, y);
|
|
}
|
|
}
|
|
|
|
private void SetBoundaryWall(int x, int y)
|
|
{
|
|
if (TypeId[x, y] == _idWall)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (TypeId[x, y] == 0)
|
|
{
|
|
ParticleCount++;
|
|
}
|
|
|
|
TypeId[x, y] = _idWall;
|
|
Temperature[x, y] = _initialTemperature[_idWall];
|
|
BurnTime[x, y] = _burnDuration[_idWall];
|
|
Burning[x, y] = _burningInit[_idWall];
|
|
SparkTime[x, y] = 0;
|
|
Lifetime[x, y] = _defaultLifetime[_idWall];
|
|
_pressureDuration[x, y] = 0f;
|
|
_cellAge[x, y] = 0f;
|
|
_integrity[x, y] = _durability[_idWall];
|
|
ExpandOccupiedBoundsToInclude(x, y);
|
|
MarkVisualDirty(x, y);
|
|
MarkProcessed(x, y);
|
|
}
|
|
|
|
private bool InBounds(int x, int y) => x >= 0 && x < Width && y >= 0 && y < Height;
|
|
|
|
private bool IsBoundary(int x, int y) => x == 0 || y == 0 || x == Width - 1 || y == Height - 1;
|
|
|
|
private bool TryNormalizeCoordinate(ref int x, ref int y)
|
|
{
|
|
if (_settings.WrapParticles)
|
|
{
|
|
x = ((x % Width) + Width) % Width;
|
|
y = ((y % Height) + Height) % Height;
|
|
return true;
|
|
}
|
|
|
|
return InBounds(x, y);
|
|
}
|
|
|
|
private void MarkProcessed(int x, int y)
|
|
{
|
|
if (_activeStepToken != 0 && InBounds(x, y))
|
|
{
|
|
_processedFrame[x, y] = _activeStepToken;
|
|
}
|
|
}
|
|
|
|
private void ForEachNeighbor8(int x, int y, Action<int, int> visitor)
|
|
{
|
|
for (var dx = -1; dx <= 1; dx++)
|
|
{
|
|
for (var dy = -1; dy <= 1; dy++)
|
|
{
|
|
if (dx == 0 && dy == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var nx = x + dx;
|
|
var ny = y + dy;
|
|
if (TryNormalizeCoordinate(ref nx, ref ny))
|
|
{
|
|
visitor(nx, ny);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static uint Hash(int x, int y, int frame)
|
|
{
|
|
unchecked
|
|
{
|
|
var seed = (uint)(x * 73856093) ^ (uint)(y * 19349663) ^ (uint)(frame * 83492791);
|
|
seed ^= seed >> 13;
|
|
return seed * 1274126177u;
|
|
}
|
|
}
|
|
|
|
private static bool Chance(uint hash, float threshold) => (hash & 0xFFFF) / 65535f < threshold;
|
|
|
|
private static bool NextChance(ref uint seed, float threshold)
|
|
{
|
|
unchecked
|
|
{
|
|
seed = (seed * 1664525u) + 1013904223u;
|
|
}
|
|
|
|
return Chance(seed, threshold);
|
|
}
|
|
|
|
private static float Clamp(float value, float min, float max)
|
|
{
|
|
if (min > max)
|
|
{
|
|
(min, max) = (max, min);
|
|
}
|
|
|
|
return MathF.Min(max, MathF.Max(min, value));
|
|
}
|
|
|
|
private void RebuildActiveFieldBounds(int startX, int startY, int endX, int endY)
|
|
{
|
|
_hasActiveFieldBounds = false;
|
|
for (var y = startY; y <= endY; y++)
|
|
{
|
|
for (var x = startX; x <= endX; x++)
|
|
{
|
|
if (HasDynamicFieldAt(x, y))
|
|
{
|
|
ExpandActiveFieldBoundsToInclude(x, y);
|
|
}
|
|
}
|
|
}
|
|
|
|
_fieldBoundsDirty = false;
|
|
}
|
|
}
|