Terrain/Terraforming/Terraformer.cs
using System;
using System.Collections.Immutable;
using HC3.Terrain;
using HC3.UI;
namespace HC3.Terraforming;
#nullable enable
/// <summary>
/// Construction tool for editing the terrain.
/// </summary>
public sealed class Terraformer : Component, IBuilder
{
public static Terraformer? Instance { get; private set; }
private TerraformMode? _currentMode;
public TerraformMode? CurrentMode
{
get => _currentMode;
set
{
if ( _currentMode == value ) return;
if ( _currentMode is not null ) _currentMode.IsActive = false;
_currentMode = value;
if ( _currentMode is not null )
{
_currentMode.IsActive = true;
Activate();
}
}
}
public void Activate()
{
Enabled = true;
WindowManager.Instance.DeactivateAll();
(this as IBuilder).DeactivateAll();
_currentMode ??= AllModes.FirstOrDefault();
if ( _currentMode is not null ) _currentMode.IsActive = true;
}
public void Deactivate()
{
if ( _currentMode is not null ) _currentMode.IsActive = false;
RemoveCursors();
Enabled = false;
}
public bool IsTerraforming => Active && CurrentMode is not null;
/// <summary>
/// Cost to raise one corner of one tile one unit.
/// </summary>
[Property]
public int UnitCost { get; set; } = 5;
/// <summary>
/// If not null, paint this material when teraaforming.
/// </summary>
[Property]
public int? MaterialIndex { get; set; }
[Property]
public GameObject? CursorPrefab { get; set; }
private readonly List<GameObject> _cursors = new();
private RectInt _tileRange;
private RectInt _cursorRange;
private TileArraySlice _originalTiles;
private TileArraySlice _modifiedTiles;
private ParkTerrain? _terrain;
private Vector2 _dragStartPos;
private int? _lastDragDelta;
private ITerraformBrush? _lastBrush;
[SkipHotload]
private ImmutableArray<TerraformMode>? _allModes;
public IReadOnlyList<TerraformMode> AllModes =>
_allModes ??= TerraformMode.GetAll().ToImmutableArray();
public Vector2 CursorGridPos { get; private set; }
protected override void OnAwake()
{
Instance = this;
Enabled = false;
}
protected override void OnDestroy()
{
if ( Instance == this ) Instance = null;
}
protected override void OnEnabled()
{
Activate();
}
protected override void OnDisabled()
{
Deactivate();
}
protected override void OnUpdate()
{
if ( Input.EscapePressed )
{
Input.EscapePressed = false;
Deactivate();
}
if ( !Input.Down( "Attack1" ) )
{
UpdateCursorPos();
}
_currentMode?.Update( CursorGridPos );
UpdateBrush();
if ( !Input.Down( "Attack1" ) )
{
UpdateCursorTiles();
}
else
{
UpdateDrag();
}
}
private void UpdateCursorPos()
{
if ( Scene.Camera is not { } camera ) return;
var ray = camera.ScreenPixelToRay( Mouse.Position );
var result = Scene.Trace
.Ray( ray, 65536f )
.UsePhysicsWorld()
.WithTag( "ground" )
.Run();
if ( !result.Hit || result.Component is not TerrainMesh { Terrain: var terrain } ) return;
CursorGridPos = terrain.WorldToGrid( result.HitPosition );
_terrain = terrain;
}
private void UpdateCursorTiles()
{
if ( CurrentMode is not { Brush: { } brush, Margin: var margin } ) return;
if ( _terrain is null ) return;
if ( !Mouse.Active ) return;
_dragStartPos = Mouse.Position;
_lastDragDelta = null;
var lastRange = _tileRange;
var min = _terrain.GetTileIndex( CursorGridPos - (brush.Size - 1) / 2f );
_cursorRange = new RectInt( min, brush.Size );
_tileRange = _cursorRange.Grow( margin );
if ( _tileRange != lastRange || brush != _lastBrush )
{
_lastBrush = brush;
UpdateCursorPosition();
}
}
private void RemoveCursors()
{
foreach ( var cursor in _cursors )
{
cursor.Destroy();
}
_cursors.Clear();
}
/// <summary>
/// Make sure we have the right number of cursors for the current brush.
/// </summary>
private void UpdateBrush()
{
if ( CurrentMode is not { Brush: { } brush, Margin: var margin } )
{
RemoveCursors();
return;
}
if ( !CursorPrefab.IsValid() ) return;
var size = brush.Size + 1;
var count = size.x * size.y;
while ( _cursors.Count > count )
{
foreach ( var cursor in _cursors.Skip( count ) )
{
cursor.Destroy();
}
_cursors.RemoveRange( count, _cursors.Count - count );
}
while ( _cursors.Count < count )
{
var cursor = CursorPrefab.Clone( global::Transform.Zero, GameObject );
_cursors.Add( cursor );
}
}
private void UpdateDrag()
{
if ( _terrain is not { } terrain ) return;
if ( CurrentMode is not { } mode ) return;
var delta = (int)MathF.Round( (_dragStartPos.y - Mouse.Position.y) / 32f );
if ( delta == _lastDragDelta ) return;
_lastDragDelta = delta;
if ( _modifiedTiles.Size != _originalTiles.Size )
{
_modifiedTiles = new TileArraySlice( _originalTiles.Size );
}
mode.Apply( _originalTiles, _modifiedTiles, new TerraformContext( terrain, _tileRange, CursorGridPos, delta ) );
var gridManager = GridManager.Instance;
var buildingZone = BuildingZone.Instance;
foreach ( var (index, tile) in _originalTiles )
{
var gridPos = index + _tileRange.Position;
if ( !buildingZone.IsOwned( gridPos ) )
{
_modifiedTiles[index] = tile;
}
else if ( gridManager.IsConstructionBlocked( gridPos ) )
{
_modifiedTiles[index] = tile with { Paint = _modifiedTiles[index].Paint };
}
}
ParkManager.Instance?.Terraform( _tileRange, _modifiedTiles.ToArray() );
UpdateCursorPositions( _modifiedTiles );
}
private void UpdateCursorPosition()
{
if ( CurrentMode?.Brush is not { } brush ) return;
if ( _terrain is not { } terrain ) return;
if ( _originalTiles.Size != _tileRange.Size )
{
_originalTiles = new TileArraySlice( _tileRange.Size );
}
terrain.CopyToClamped( _tileRange, _originalTiles );
UpdateCursorPositions( _originalTiles );
}
private void UpdateCursorPositions( TileArraySlice tiles )
{
if ( CurrentMode is not { Brush: { } brush, Margin: var margin } ) return;
if ( _terrain is not { } terrain ) return;
tiles = tiles.Slice( new RectInt( margin, brush.Size ) );
var size = tiles.Size + 1;
var count = size.x * size.y;
var gridManager = GridManager.Instance;
var buildingZone = BuildingZone.Instance;
Span<int> cursorHeights = stackalloc int[count];
foreach ( var (tileIndex, tile) in tiles )
{
var gridPos = tileIndex + _cursorRange.Position;
var index = tileIndex.x + tileIndex.y * size.x;
var canBuild = !gridManager.IsConstructionBlocked( gridPos ) && buildingZone.IsOwned( gridPos );
cursorHeights[index] = Math.Max( cursorHeights[index], canBuild ? tile.GetCornerHeight( TileCorner.XMinYMin ) : -1 );
cursorHeights[index + 1] = Math.Max( cursorHeights[index + 1], canBuild ? tile.GetCornerHeight( TileCorner.XMaxYMin ) : -1 );
cursorHeights[index + size.x] = Math.Max( cursorHeights[index + size.x], canBuild ? tile.GetCornerHeight( TileCorner.XMinYMax ) : -1 );
cursorHeights[index + size.x + 1] = Math.Max( cursorHeights[index + size.x + 1], canBuild ? tile.GetCornerHeight( TileCorner.XMaxYMax ) : -1 );
}
for ( var i = 0; i < count; ++i )
{
var cursor = _cursors[i];
var x = i % size.x;
var y = i / size.x;
var height = cursorHeights[i];
var gridPos = new Vector2( x, y ) + _cursorRange.Position;
var weight = brush.GetWeight( new Vector2Int( x, y ) );
cursor.Enabled = weight > 0f;
cursor.WorldPosition = terrain.GridToWorld( new Vector3( gridPos, height ) );
cursor.WorldScale = 0.25f + weight * 0.75f;
}
}
}