Terrain/Terraforming/MountainMode.cs
using HC3.Terrain;
using Sandbox.Utility;
using System;

namespace HC3.Terraforming;

#nullable enable

public sealed class MountainMode : TerraformMode
{
	public override string Title => "Mountain Tool";
	public override string Description => "Make a mountain out of a molehill.";
	public override string Icon => "landscape";
	public override int Order => 1;

	public const float MinRadius = 1;
	public const float MaxRadius = 16;

	public const int MaxGradient = 2;

	private float _radius = 1;

	[Property, Range( min: MinRadius, max: MaxRadius ), Step( 0.5f )]
	public float InnerRadius
	{
		get => _radius;
		set
		{
			value = Math.Clamp( value, MinRadius, MaxRadius );

			// ReSharper disable once CompareOfFloatsByEqualityOperator
			if ( _radius == value ) return;

			_radius = value;
			Brush = new RadialBrush( value );
		}
	}

	public float OuterRadius => InnerRadius + Margin;

	public MountainMode()
	{
		Margin = 8;
	}

	protected override void OnActivate()
	{
		Brush ??= new RadialBrush( InnerRadius );
	}

	protected override void OnUpdate()
	{
		if ( Input.Down( "Run" ) )
		{
			InnerRadius += Math.Sign( Input.MouseWheel.y ) * 0.5f;
		}
	}

	protected override void OnApply( TileArraySlice original, TileArraySlice modified, TerraformContext context )
	{
		Span<int> heights = stackalloc int[4];
		Span<int> materials = stackalloc int[4];

		var materialIndex = Terraformer.Instance?.MaterialIndex;
		var center = original.Size / 2f;

		if ( context.Delta == 0 )
		{
			original.CopyTo( modified );

			if ( materialIndex is { } matIndex )
			{
				foreach ( var (index, originalTile) in original )
				{
					for ( var i = 0; i < 4; ++i )
					{
						var corner = (TileCorner)i;
						var gridPos = index + corner.GetHorizontalOffset();
						var dist = (gridPos - center).Length;

						materials[i] = dist < InnerRadius
							? matIndex
							: originalTile.Paint.GetMaterialIndex( corner );
					}

					modified[index] = originalTile with { Paint = TilePaint.FromMaterialIndices( materials ) };
				}
			}

			return;
		}

		var margin = (int)MathF.Floor( OuterRadius - InnerRadius );

		var sign = Math.Sign( context.Delta );

		var tileset = context.Terrain.Tileset;
		var minDist = int.MaxValue;
		var maxDist = int.MinValue;

		foreach ( var (index, originalTile) in original )
		{
			for ( var i = 0; i < 4; ++i )
			{
				var corner = (TileCorner)i;
				var gridPos = index + corner.GetHorizontalOffset();
				var height = originalTile.GetCornerHeight( corner );

				var target = -sign * GetOffset( (gridPos - center).Length );
				var dist = sign * (height - target);

				minDist = Math.Min( minDist, dist );
				maxDist = Math.Max( maxDist, dist );
			}
		}

		var delta = Math.Clamp( context.Delta, -margin * 4, +margin * 4 );

		foreach ( var (index, originalTile) in original )
		{
			for ( var i = 0; i < 4; ++i )
			{
				var corner = (TileCorner)i;
				var gridPos = index + corner.GetHorizontalOffset();
				var cornerStartHeight = originalTile.GetCornerHeight( corner );
				var dist = (gridPos - center).Length;

				var target = sign * minDist + delta - sign * GetOffset( dist );

				heights[i] = delta > 0
					? Math.Max( cornerStartHeight, target )
					: Math.Min( cornerStartHeight, target );

				materials[i] = dist < InnerRadius
					? materialIndex ?? originalTile.Paint.GetMaterialIndex( corner )
					: originalTile.Paint.GetMaterialIndex( corner );
			}

			modified[index] = tileset.FromCornerHeights( heights, TilePaint.FromMaterialIndices( materials ) );
		}
	}

	private int GetOffset( float dist )
	{
		var offset = dist < InnerRadius
			? Easing.EaseIn( dist / InnerRadius ) * InnerRadius * MaxGradient * 0.5f
			: dist * MaxGradient - InnerRadius * MaxGradient * 0.5f;

		return (int)MathF.Round( offset );
	}
}