563 lines
22 KiB
C#
563 lines
22 KiB
C#
using Sand.ChunkPrototype;
|
|
using Sand.Core;
|
|
|
|
namespace Sand.App;
|
|
|
|
internal sealed class ChunkPrototypeSimulationBackend : ISimulationBackend
|
|
{
|
|
private readonly PrototypeSparseSandAdapter _adapter;
|
|
private readonly SimulationSettings _settings;
|
|
private readonly ParticleLibrary _library;
|
|
private readonly Dictionary<string, PrototypeParticle> _particleProfiles;
|
|
private readonly PrototypeParticle _wallParticle;
|
|
private readonly AppSimulationFrameStats _frameStats = new();
|
|
private int _frame;
|
|
private int _trimCounter;
|
|
|
|
public ChunkPrototypeSimulationBackend(int width, int height, int particleSize, IParticleLibrary library, SimulationSettings settings)
|
|
{
|
|
_adapter = new PrototypeSparseSandAdapter(width, height, settings.AmbientTemperature);
|
|
_settings = settings;
|
|
_library = library as ParticleLibrary ?? throw new ArgumentException("Expected ParticleLibrary implementation.", nameof(library));
|
|
_particleProfiles = BuildParticleProfiles(_library);
|
|
foreach (var particle in _particleProfiles.Values)
|
|
{
|
|
_adapter.RegisterParticleProfile(particle);
|
|
}
|
|
|
|
_wallParticle = _particleProfiles.GetValueOrDefault("wall");
|
|
ParticleSize = particleSize;
|
|
RefreshSettingsState();
|
|
}
|
|
|
|
public string BackendName => "chunk";
|
|
public SimulationSettings Settings => _settings;
|
|
public AppSimulationFrameStats FrameStats => _frameStats;
|
|
public int Frame => _frame;
|
|
public int ParticleCount => _adapter.ParticleCount;
|
|
public int ParticleSize { get; }
|
|
|
|
public ReadOnlySpan<byte> BuildRgbaFrame()
|
|
{
|
|
var frame = _adapter.BuildRgbaFrame(_settings.EnableWindVisuals, _settings.EnablePressureVisuals);
|
|
var stats = _adapter.LastStepStats;
|
|
_frameStats.VisualDirtyPageCount = stats.VisualDirtyPages;
|
|
_frameStats.FrameBuildBytesTouched = stats.FrameBuildBytesTouched;
|
|
_frameStats.RenderTimeMicroseconds = stats.RenderTimeMicroseconds;
|
|
return frame;
|
|
}
|
|
|
|
public void Step(float dt)
|
|
{
|
|
if (_settings.PauseSim)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_adapter.World.ClearDirtyChunks();
|
|
var processed = _adapter.Step();
|
|
_frame++;
|
|
_trimCounter++;
|
|
if (_trimCounter >= 30)
|
|
{
|
|
_adapter.TrimResidency(marginChunks: 1);
|
|
_trimCounter = 0;
|
|
}
|
|
|
|
_frameStats.Frame = _frame;
|
|
_frameStats.ProcessedCells = Math.Max(processed, _adapter.ParticleCount);
|
|
_frameStats.ParticleCount = _adapter.ParticleCount;
|
|
_frameStats.LoadedChunkCount = _adapter.World.LoadedChunkCount;
|
|
_frameStats.ActiveChunkCount = _adapter.World.ActiveChunkCount;
|
|
_frameStats.DirtyChunkCount = _adapter.World.DirtyChunkCount;
|
|
_frameStats.SteppedChunkCount = _adapter.LastStepStats.SteppedChunks;
|
|
_frameStats.SleepingChunkCount = _adapter.LastStepStats.SleepingChunks;
|
|
_frameStats.FieldPageCount = _adapter.LastStepStats.FieldPages;
|
|
_frameStats.MoveAttemptCount = _adapter.LastStepStats.MoveAttempts;
|
|
_frameStats.VerticalMoveAttemptCount = _adapter.LastStepStats.VerticalMoveAttempts;
|
|
_frameStats.DiagonalMoveAttemptCount = _adapter.LastStepStats.DiagonalMoveAttempts;
|
|
_frameStats.LateralMoveAttemptCount = _adapter.LastStepStats.LateralMoveAttempts;
|
|
_frameStats.SuccessfulMoveCount = _adapter.LastStepStats.SuccessfulMoves;
|
|
_frameStats.SwapAttemptCount = _adapter.LastStepStats.SwapAttempts;
|
|
_frameStats.StalledMovableCount = _adapter.LastStepStats.StalledMovableCells;
|
|
_frameStats.MovementOnlyFastPathCount = _adapter.LastStepStats.MovementOnlyFastPathCount;
|
|
_frameStats.FullRuntimeStepCount = _adapter.LastStepStats.FullRuntimeStepCount;
|
|
_frameStats.FullRuntimeSolidCount = _adapter.LastStepStats.FullRuntimeSolidCount;
|
|
_frameStats.FullRuntimeLiquidCount = _adapter.LastStepStats.FullRuntimeLiquidCount;
|
|
_frameStats.FullRuntimeGasCount = _adapter.LastStepStats.FullRuntimeGasCount;
|
|
_frameStats.MovedParticleCount = _adapter.LastStepStats.MovedParticles;
|
|
_frameStats.SwappedParticleCount = _adapter.LastStepStats.SwappedParticles;
|
|
_frameStats.VisualDirtyPageCount = _adapter.LastStepStats.VisualDirtyPages;
|
|
_frameStats.ActivationTimeMicroseconds = _adapter.LastStepStats.ActivationTimeMicroseconds;
|
|
_frameStats.MovementTimeMicroseconds = _adapter.LastStepStats.MovementTimeMicroseconds;
|
|
_frameStats.RuntimeTimeMicroseconds = _adapter.LastStepStats.RuntimeTimeMicroseconds;
|
|
_frameStats.FieldDecayTimeMicroseconds = _adapter.LastStepStats.FieldDecayTimeMicroseconds;
|
|
_frameStats.RenderTimeMicroseconds = _adapter.LastStepStats.RenderTimeMicroseconds;
|
|
UpdateBoundsStats();
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
foreach (var (x, y) in _adapter.Particles.ToArray())
|
|
{
|
|
_adapter.RemoveParticle(x, y);
|
|
}
|
|
|
|
_adapter.ClearFields();
|
|
_frame = 0;
|
|
_trimCounter = 0;
|
|
_frameStats.Frame = 0;
|
|
_frameStats.ProcessedCells = 0;
|
|
_frameStats.ParticleCount = 0;
|
|
_frameStats.MinActiveX = 0;
|
|
_frameStats.MinActiveY = 0;
|
|
_frameStats.MaxActiveX = 0;
|
|
_frameStats.MaxActiveY = 0;
|
|
_frameStats.LoadedChunkCount = 0;
|
|
_frameStats.ActiveChunkCount = 0;
|
|
_frameStats.DirtyChunkCount = 0;
|
|
_frameStats.SteppedChunkCount = 0;
|
|
_frameStats.SleepingChunkCount = 0;
|
|
_frameStats.FieldPageCount = 0;
|
|
_frameStats.MoveAttemptCount = 0;
|
|
_frameStats.VerticalMoveAttemptCount = 0;
|
|
_frameStats.DiagonalMoveAttemptCount = 0;
|
|
_frameStats.LateralMoveAttemptCount = 0;
|
|
_frameStats.SuccessfulMoveCount = 0;
|
|
_frameStats.SwapAttemptCount = 0;
|
|
_frameStats.StalledMovableCount = 0;
|
|
_frameStats.MovementOnlyFastPathCount = 0;
|
|
_frameStats.FullRuntimeStepCount = 0;
|
|
_frameStats.FullRuntimeSolidCount = 0;
|
|
_frameStats.FullRuntimeLiquidCount = 0;
|
|
_frameStats.FullRuntimeGasCount = 0;
|
|
_frameStats.MovedParticleCount = 0;
|
|
_frameStats.SwappedParticleCount = 0;
|
|
_frameStats.VisualDirtyPageCount = 0;
|
|
_frameStats.FrameBuildBytesTouched = 0;
|
|
_frameStats.ActivationTimeMicroseconds = 0;
|
|
_frameStats.MovementTimeMicroseconds = 0;
|
|
_frameStats.RuntimeTimeMicroseconds = 0;
|
|
_frameStats.FieldDecayTimeMicroseconds = 0;
|
|
_frameStats.RenderTimeMicroseconds = 0;
|
|
RefreshSettingsState();
|
|
}
|
|
|
|
public void RefreshSettingsState()
|
|
{
|
|
if (_settings.OuterWall)
|
|
{
|
|
for (var x = 0; x < _adapter.Width; x++)
|
|
{
|
|
_adapter.AddParticle(x, 0, _wallParticle);
|
|
_adapter.AddParticle(x, _adapter.Height - 1, _wallParticle);
|
|
}
|
|
|
|
for (var y = 0; y < _adapter.Height; y++)
|
|
{
|
|
_adapter.AddParticle(0, y, _wallParticle);
|
|
_adapter.AddParticle(_adapter.Width - 1, y, _wallParticle);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (var x = 0; x < _adapter.Width; x++)
|
|
{
|
|
RemoveBoundaryWallIfPresent(x, 0);
|
|
RemoveBoundaryWallIfPresent(x, _adapter.Height - 1);
|
|
}
|
|
|
|
for (var y = 0; y < _adapter.Height; y++)
|
|
{
|
|
RemoveBoundaryWallIfPresent(0, y);
|
|
RemoveBoundaryWallIfPresent(_adapter.Width - 1, y);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ClearParticleCircle(int centerX, int centerY, int brushRadius)
|
|
{
|
|
PaintCircle(centerX, centerY, brushRadius, particleId: null);
|
|
}
|
|
|
|
public void ClearParticlePourAtPixel(int centerX, int centerY, int brushRadius, int maxParticles, int seed)
|
|
{
|
|
PaintCirclePour(centerX, centerY, brushRadius, maxParticles, seed, particleId: null);
|
|
}
|
|
|
|
public void CreateParticleCircle(int centerX, int centerY, int brushRadius, string particleId)
|
|
{
|
|
PaintCircle(centerX, centerY, brushRadius, particleId);
|
|
}
|
|
|
|
public void CreateParticlePourAtPixel(int centerX, int centerY, int brushRadius, string particleId, int maxParticles, int seed)
|
|
{
|
|
PaintCirclePour(centerX, centerY, brushRadius, maxParticles, seed, particleId);
|
|
}
|
|
|
|
private void PaintCirclePour(int centerX, int centerY, int brushRadius, int maxParticles, int seed, string? particleId)
|
|
{
|
|
var gridCenterX = centerX / ParticleSize;
|
|
var gridCenterY = centerY / ParticleSize;
|
|
var diameter = (brushRadius * 2) + 1;
|
|
var touched = 0;
|
|
for (var i = 0; i < Math.Max(maxParticles * 6, diameter * diameter) && touched < maxParticles; i++)
|
|
{
|
|
var dx = ((seed + (i * 17)) % diameter) - brushRadius;
|
|
var dy = ((seed + (i * 31)) % diameter) - brushRadius;
|
|
if ((dx * dx) + (dy * dy) > brushRadius * brushRadius)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (particleId is null)
|
|
{
|
|
var x = gridCenterX + dx;
|
|
var y = gridCenterY + dy;
|
|
if (TryNormalizeCoordinate(ref x, ref y) && (!_settings.OuterWall || !IsBoundary(x, y)) && _adapter.RemoveParticle(x, y))
|
|
{
|
|
touched++;
|
|
}
|
|
}
|
|
else if (TryPaintAtCell(gridCenterX + dx, gridCenterY + dy, particleId))
|
|
{
|
|
touched++;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ApplyWindBrushAtPixel(int centerX, int centerY, int brushRadius, float forceX, float forceY)
|
|
{
|
|
_adapter.ApplyWindBrush(centerX / ParticleSize, centerY / ParticleSize, brushRadius, forceX, forceY);
|
|
}
|
|
|
|
public void ApplyAirBrushAtPixel(int centerX, int centerY, int brushRadius, float forceX, float forceY)
|
|
{
|
|
_adapter.ApplyAirBrush(centerX / ParticleSize, centerY / ParticleSize, brushRadius, forceX, forceY);
|
|
}
|
|
|
|
public void ApplyGravityBrushAtPixel(int centerX, int centerY, int brushRadius, float strength)
|
|
{
|
|
_adapter.ApplyGravityBrush(centerX / ParticleSize, centerY / ParticleSize, brushRadius, strength);
|
|
}
|
|
|
|
public void ApplyRepulsorBrushAtPixel(int centerX, int centerY, int brushRadius, float strength)
|
|
{
|
|
_adapter.ApplyRepulsorBrush(centerX / ParticleSize, centerY / ParticleSize, brushRadius, strength);
|
|
}
|
|
|
|
private void PaintCircle(int centerX, int centerY, int brushRadius, string? particleId)
|
|
{
|
|
var gridCenterX = centerX / ParticleSize;
|
|
var gridCenterY = centerY / ParticleSize;
|
|
for (var dx = -brushRadius; dx <= brushRadius; dx++)
|
|
{
|
|
for (var dy = -brushRadius; dy <= brushRadius; dy++)
|
|
{
|
|
if ((dx * dx) + (dy * dy) > brushRadius * brushRadius)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (particleId is null)
|
|
{
|
|
var x = gridCenterX + dx;
|
|
var y = gridCenterY + dy;
|
|
if (TryNormalizeCoordinate(ref x, ref y) && (!_settings.OuterWall || !IsBoundary(x, y)))
|
|
{
|
|
_adapter.RemoveParticle(x, y);
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
TryPaintAtCell(gridCenterX + dx, gridCenterY + dy, particleId);
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool TryPaintAtCell(int x, int y, string particleId)
|
|
{
|
|
if (!TryNormalizeCoordinate(ref x, ref y))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!_particleProfiles.TryGetValue(particleId, out var particle) || particle.IsEmpty)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (_settings.OuterWall && particle.MotionType != PrototypeParticleType.Wall && IsBoundary(x, y))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return _adapter.AddParticle(x, y, particle);
|
|
}
|
|
|
|
private void RemoveBoundaryWallIfPresent(int x, int y)
|
|
{
|
|
if (_adapter.GetParticleTypeAt(x, y) == PrototypeParticleType.Wall)
|
|
{
|
|
_adapter.RemoveParticle(x, y);
|
|
}
|
|
}
|
|
|
|
private void UpdateBoundsStats()
|
|
{
|
|
if (_adapter.ParticleCount == 0)
|
|
{
|
|
_frameStats.MinActiveX = 0;
|
|
_frameStats.MinActiveY = 0;
|
|
_frameStats.MaxActiveX = 0;
|
|
_frameStats.MaxActiveY = 0;
|
|
return;
|
|
}
|
|
|
|
var minX = int.MaxValue;
|
|
var minY = int.MaxValue;
|
|
var maxX = int.MinValue;
|
|
var maxY = int.MinValue;
|
|
foreach (var ((x, y), _) in _adapter.ParticleEntries)
|
|
{
|
|
minX = Math.Min(minX, x);
|
|
minY = Math.Min(minY, y);
|
|
maxX = Math.Max(maxX, x);
|
|
maxY = Math.Max(maxY, y);
|
|
}
|
|
|
|
_frameStats.MinActiveX = minX;
|
|
_frameStats.MinActiveY = minY;
|
|
_frameStats.MaxActiveX = maxX;
|
|
_frameStats.MaxActiveY = maxY;
|
|
}
|
|
|
|
private bool TryNormalizeCoordinate(ref int x, ref int y)
|
|
{
|
|
if (_settings.WrapParticles)
|
|
{
|
|
x = ((x % _adapter.Width) + _adapter.Width) % _adapter.Width;
|
|
y = ((y % _adapter.Height) + _adapter.Height) % _adapter.Height;
|
|
return true;
|
|
}
|
|
|
|
return x >= 0 && x < _adapter.Width && y >= 0 && y < _adapter.Height;
|
|
}
|
|
|
|
private bool IsBoundary(int x, int y) => x == 0 || y == 0 || x == _adapter.Width - 1 || y == _adapter.Height - 1;
|
|
|
|
private static Dictionary<string, PrototypeParticle> BuildParticleProfiles(ParticleLibrary library)
|
|
{
|
|
var idLookup = new Dictionary<string, ushort>(StringComparer.OrdinalIgnoreCase);
|
|
for (ushort typeId = 1; typeId <= library.Definitions.Count; typeId++)
|
|
{
|
|
var definition = library.GetDefinition(typeId);
|
|
idLookup[definition.Id] = typeId;
|
|
}
|
|
|
|
var result = new Dictionary<string, PrototypeParticle>(StringComparer.OrdinalIgnoreCase);
|
|
for (ushort typeId = 1; typeId <= library.Definitions.Count; typeId++)
|
|
{
|
|
var definition = library.GetDefinition(typeId);
|
|
result[definition.Id] = CreateParticleProfile(typeId, definition, idLookup);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private static PrototypeParticle CreateParticleProfile(ushort typeId, ParticleDef definition, IReadOnlyDictionary<string, ushort> idLookup)
|
|
{
|
|
var motionType = ResolveMotionType(definition);
|
|
if (motionType == PrototypeParticleType.Empty)
|
|
{
|
|
return default;
|
|
}
|
|
|
|
var runtime = ParticleRuntimeProfileBuilder.Build(definition);
|
|
var velocity = definition.Velocity > 0f
|
|
? definition.Velocity
|
|
: definition.Kind switch
|
|
{
|
|
ParticleKind.Solid => 0.45f,
|
|
ParticleKind.Liquid => 0.35f,
|
|
ParticleKind.Gas => 0.25f,
|
|
_ => 0.35f,
|
|
};
|
|
|
|
var friction = definition.Friction;
|
|
var viscosity = definition.Viscosity;
|
|
switch (motionType)
|
|
{
|
|
case PrototypeParticleType.Sand:
|
|
velocity = MathF.Max(0.5f, velocity);
|
|
friction = Math.Clamp(friction * 0.75f, 0f, 1f);
|
|
viscosity = Math.Clamp(viscosity * 0.75f, 0f, 1.25f);
|
|
break;
|
|
case PrototypeParticleType.Water:
|
|
velocity = MathF.Max(0.4f, velocity);
|
|
friction = Math.Clamp(friction * 0.6f, 0f, 1f);
|
|
viscosity = Math.Clamp(viscosity * 0.65f, 0f, 1.25f);
|
|
break;
|
|
case PrototypeParticleType.Steam:
|
|
velocity = MathF.Max(0.45f, velocity + (runtime.Balance.UpwardBias * 0.35f));
|
|
friction = Math.Clamp(friction * 0.4f, 0f, 0.75f);
|
|
viscosity = Math.Clamp(viscosity * 0.5f, 0f, 0.85f);
|
|
break;
|
|
}
|
|
|
|
var flags = PrototypeParticleFlags.None;
|
|
if (definition.Id is "water" or "wet_sand" or "mud")
|
|
{
|
|
flags |= PrototypeParticleFlags.WaterLike;
|
|
}
|
|
|
|
if (definition.Id is "fire" or "plasma" or "ember" or "energy")
|
|
{
|
|
flags |= PrototypeParticleFlags.FireLike | PrototypeParticleFlags.HotSource;
|
|
}
|
|
|
|
if (definition.Id == "lava" || definition.Id == "burning_wood" || definition.Id.StartsWith("molten_", StringComparison.Ordinal))
|
|
{
|
|
flags |= PrototypeParticleFlags.HotSource;
|
|
}
|
|
|
|
if (definition.Id == "acid")
|
|
{
|
|
flags |= PrototypeParticleFlags.Acidic;
|
|
}
|
|
|
|
var hydrateTargetTypeId = definition.Id switch
|
|
{
|
|
"sand" when idLookup.TryGetValue("wet_sand", out var wetSand) => wetSand,
|
|
"dirt" when idLookup.TryGetValue("mud", out var mud) => mud,
|
|
_ => (ushort)0,
|
|
};
|
|
|
|
var pressureResponse = MathF.Max(0.15f, runtime.Balance.PressureSensitivity);
|
|
|
|
return new PrototypeParticle(
|
|
TypeId: typeId,
|
|
Id: definition.Id,
|
|
MotionType: motionType,
|
|
Kind: definition.Kind,
|
|
BehaviorKind: runtime.Balance.BehaviorKind,
|
|
R: definition.Color.R,
|
|
G: definition.Color.G,
|
|
B: definition.Color.B,
|
|
Mass: MathF.Max(0.05f, definition.Mass),
|
|
Velocity: Math.Clamp(velocity, 0.05f, 1.4f),
|
|
Friction: Math.Clamp(friction, 0f, 1.5f),
|
|
Viscosity: Math.Clamp(viscosity, 0f, 2f),
|
|
IsStatic: definition.IsStatic || motionType == PrototypeParticleType.Wall,
|
|
Flags: flags,
|
|
IsMolten: definition.Id == "lava" || definition.Id.StartsWith("molten_", StringComparison.Ordinal),
|
|
HydrateTargetTypeId: hydrateTargetTypeId,
|
|
MeltTypeId: ResolveOptionalTarget(idLookup, definition.Melt),
|
|
EvaporateTypeId: ResolveOptionalTarget(idLookup, definition.Evaporate),
|
|
SolidifyTypeId: ResolveOptionalTarget(idLookup, definition.Solidify),
|
|
FreezeTypeId: ResolveOptionalTarget(idLookup, definition.Freeze),
|
|
BrokenTypeId: ResolveOptionalTarget(idLookup, definition.Broken),
|
|
PressureThreshold: definition.PressureThreshold,
|
|
PressureResistance: definition.PressureResistance,
|
|
PressureTolerance: definition.PressureTolerance,
|
|
PressureThresholdDuration: checked((short)Math.Clamp(definition.PressureThresholdDuration, 0, short.MaxValue)),
|
|
PressureResponse: pressureResponse,
|
|
ForceResponseMultiplier: MathF.Max(0.1f, runtime.Balance.ForceResponseMultiplier),
|
|
LateralFlowMultiplier: MathF.Max(0.05f, runtime.Balance.LateralFlowMultiplier),
|
|
DiagonalFlowMultiplier: MathF.Max(0.05f, runtime.Balance.DiagonalFlowMultiplier),
|
|
UpwardBias: runtime.Balance.UpwardBias,
|
|
SideDriftBias: runtime.Balance.SideDriftBias,
|
|
InitialTemperature: definition.Temperature,
|
|
MeltTemperature: definition.MeltTemperature ?? float.PositiveInfinity,
|
|
EvaporateTemperature: definition.EvaporateTemperature ?? float.PositiveInfinity,
|
|
SolidifyTemperature: definition.SolidifyTemperature ?? float.NegativeInfinity,
|
|
FreezeTemperature: definition.FreezeTemperature ?? float.NegativeInfinity,
|
|
BurnDuration: definition.BurnDuration,
|
|
BurnTemperature: definition.BurnTemperature,
|
|
BurnRate: MathF.Max(0.05f, definition.BurnRate),
|
|
BurningInit: definition.Burning,
|
|
DefaultLifetime: ResolveDefaultLifetime(definition, runtime.Balance),
|
|
HeatEmission: definition.HeatEmission * MathF.Max(0.01f, runtime.Balance.HeatEmissionMultiplier),
|
|
SmokeSpawnChance: runtime.Balance.SmokeSpawnChance,
|
|
EmberSpawnChance: runtime.Balance.EmberSpawnChance,
|
|
Hardness: definition.Hardness,
|
|
Durability: definition.Durability,
|
|
Flamability: definition.Flamability,
|
|
Conductivity: definition.Conductivity,
|
|
Conductive: definition.Conductive,
|
|
AmbientCoolingMultiplier: runtime.Balance.AmbientCoolingMultiplier,
|
|
NeighborHeatTransferMultiplier: runtime.Balance.NeighborHeatTransferMultiplier,
|
|
PhaseTransitionHysteresis: runtime.Balance.PhaseTransitionHysteresis,
|
|
ProduceTypeId: ResolveOptionalTarget(idLookup, definition.Produces),
|
|
ProducesOnDeathTypeId: ResolveOptionalTarget(idLookup, definition.ProducesOnDeath));
|
|
}
|
|
|
|
private static ushort ResolveOptionalTarget(IReadOnlyDictionary<string, ushort> idLookup, string? particleId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(particleId))
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
return idLookup.TryGetValue(particleId, out var typeId) ? typeId : (ushort)0;
|
|
}
|
|
|
|
private static 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 static PrototypeParticleType ResolveMotionType(ParticleDef definition)
|
|
{
|
|
if (definition.Id is "air" or "wind" or "gravity_well" or "repulsor")
|
|
{
|
|
return PrototypeParticleType.Empty;
|
|
}
|
|
|
|
if (definition.Id == "wall" || definition.IsStatic)
|
|
{
|
|
return PrototypeParticleType.Wall;
|
|
}
|
|
|
|
if (definition.Id is "fire" or "plasma" or "smoke" or "steam" or "spark" or "energy")
|
|
{
|
|
return PrototypeParticleType.Steam;
|
|
}
|
|
|
|
if (definition.Id is "burning_wood")
|
|
{
|
|
return PrototypeParticleType.Wall;
|
|
}
|
|
|
|
if (definition.Id is "ember" or "snow")
|
|
{
|
|
return PrototypeParticleType.Sand;
|
|
}
|
|
|
|
return definition.Kind switch
|
|
{
|
|
ParticleKind.Gas => PrototypeParticleType.Steam,
|
|
ParticleKind.Liquid => PrototypeParticleType.Water,
|
|
ParticleKind.Solid => PrototypeParticleType.Sand,
|
|
_ => PrototypeParticleType.Empty,
|
|
};
|
|
}
|
|
}
|