Park/Buildings/BuildingPlacer.cs
using HC3.Terrain;
using HC3.UI;
using System;
using HC3.Rides;
namespace HC3;
public sealed class BuildingPlacer : Component, IBuilder
{
public static BuildingPlacer Instance { get; private set; } = null;
[Property] public GameObject EntrancePrefab { get; set; }
[Property] public GameObject ExitPrefab { get; set; }
[Property] public Color RedChannel { get; private set; } = Color.White;
[Property] public Color GreenChannel { get; private set; } = Color.White;
[Property] public Color BlueChannel { get; private set; } = Color.White;
[Property] public Color AlphaChannel { get; private set; } = Color.White;
public bool IsPlacing => ObjectToPlace.IsValid();
private bool _isDestroying = false;
public bool IsDestroying
{
get => _isDestroying;
set
{
_isDestroying = value;
if ( _isDestroying )
{
StopPlacing();
WindowManager.Instance.DeactivateAll();
}
else
{
ClearHoverGhost();
}
}
}
public IPlacementObject PlacingResource { get; private set; } = null;
public IPlacementObject ObjectToPlace { get; private set; } = null;
public BasicRide RideContext { get; private set; }
public Vector3 PlacingPosition { get; private set; } = 0;
public TileEdge PlacingEdge { get; private set; }
public float DecorationHeightOffset { get; set; } = 0f;
int _angle = 0;
int _pitch = 0;
int _height;
Color _decorationColor = Color.White;
public void Activate()
{
(this as IBuilder).DeactivateAll();
}
public void Deactivate()
{
IsDestroying = false;
StopPlacing();
}
GameObject HeightTool { get; set; } = Scene.GetPrefab( "prefabs/gameplay/height_tool.prefab" );
GameObject _heightTool = null;
GameObject _lastHoveredObject = null;
protected override void OnAwake()
{
Instance = this;
}
private void SetupGhostVisuals( GameObject go )
{
foreach ( var renderer in go.Components.GetAll<ModelRenderer>( FindMode.EverythingInSelfAndDescendants ) )
{
if ( renderer.SceneObject is null ) continue;
renderer.SceneObject.Attributes.Set( "Ghost", 1 );
renderer.SceneObject.Batchable = false;
}
}
private void ClearHoverGhost()
{
if ( _lastHoveredObject != null )
{
foreach ( var renderer in _lastHoveredObject.Components.GetAll<ModelRenderer>( FindMode.EverythingInSelfAndDescendants ) )
{
renderer.SceneObject?.Attributes.Set( "Ghost", 0 );
}
_lastHoveredObject = null;
}
}
public void StartPlacing( IPlacementObject placable )
{
IsDestroying = false;
if ( IsPlacing && placable == PlacingResource ) return;
StopPlacing();
PlacingResource = placable;
ObjectToPlace = placable.GameObject.Clone().GetComponent<IPlacementObject>();
ObjectToPlace.IsPlaced = false;
SetupGhostVisuals( ObjectToPlace.GameObject );
WindowManager.Instance.DeactivateAll();
}
public void StopPlacing()
{
if ( ObjectToPlace.IsValid() )
{
ObjectToPlace.GameObject.Destroy();
}
ObjectToPlace = null;
PlacingResource = null;
RideContext = null;
_lastAutoRotatePos = default;
}
private void ShowDestroyDebug( SceneTraceResult tr )
{
var go = tr.GameObject;
var worldPos = go.Root.WorldPosition;
var screenPos = Scene.Camera.PointToScreenPixels( worldPos );
if ( go.Components.TryGet<TerrainScenery>( out var scenery ) )
{
DebugOverlay.ScreenText( screenPos, $"DESTROY\n{GameUtils.Currency}{scenery.DestructionCost}", 14, TextFlag.CenterBottom, Color.Red );
}
}
private void TryDestroyObject( SceneTraceResult tr )
{
var go = tr.GameObject;
if ( go.Root.Components.TryGet<Building>( out var building ) )
{
if ( Input.Keyboard.Down( "shift" ) )
{
ParkManager.Instance?.DestroyObject( building );
}
else
{
Query.Create( "Delete Building", "Are you sure you want to delete this?", () => ParkManager.Instance?.DestroyObject( building ) );
}
}
else if ( go.Components.TryGet<TerrainScenery>( out var scenery ) && ParkManager.Instance.Money >= scenery.DestructionCost )
{
ParkManager.Instance?.DestroyObject( scenery );
}
else if ( go.Components.TryGet<PathFurniture>( out var furniture, FindMode.EverythingInSelfAndParent ) )
{
ParkManager.Instance?.DestroyObject( furniture );
}
else if ( go.Root.Components.TryGet<Decoration>( out var deco ) )
{
ParkManager.Instance?.DestroyObject( deco );
}
else if ( go.Components.TryGet<Path>( out var path ) )
{
PathBuilder.Instance.DeletePath( path );
}
}
protected override void OnUpdate()
{
if ( !ObjectToPlace.IsValid() && !IsDestroying )
return;
if ( Input.EscapePressed )
{
Input.EscapePressed = false;
IsDestroying = false;
StopPlacing();
return;
}
if ( Input.Keyboard.Down( "MOUSE2" ) || Input.Keyboard.Down( "MOUSE3" ) )
{
if ( ObjectToPlace.IsValid() )
{
ClearHoverGhost();
ObjectToPlace.GameObject.Enabled = false;
}
return;
}
else
{
if ( ObjectToPlace.IsValid() )
{
ObjectToPlace.GameObject.Enabled = true;
SetupGhostVisuals( ObjectToPlace.GameObject );
}
}
var trace = Scene.Trace.Ray( Scene.Camera.ScreenPixelToRay( Mouse.Position ), 4000f )
.UsePhysicsWorld()
.HitTriggers()
.WithAnyTags( IsDestroying ? ["building", "decoration", "furniture", "path", "area"] : ["path", "ground"] )
.Run();
if ( IsDestroying )
{
if ( trace.Hit )
{
var hovered = trace.GameObject;
if ( hovered != _lastHoveredObject )
{
ClearHoverGhost();
foreach ( var renderer in hovered.Components.GetAll<ModelRenderer>( FindMode.EverythingInSelfAndDescendants ) )
{
renderer.SceneObject.Batchable = false;
renderer.SceneObject?.Attributes.Set( "Ghost", 2 );
}
_lastHoveredObject = hovered;
}
ShowDestroyDebug( trace );
if ( Input.Pressed( "Attack1" ) )
TryDestroyObject( trace );
}
else
{
ClearHoverGhost();
}
return;
}
TryAutoRotate();
if ( CanRotate )
{
if ( Input.Pressed( "Menu" ) )
{
if ( ObjectToPlace is Decoration && Input.Down( "run" ) )
{
_pitch += 90;
if ( _pitch >= 270 ) _pitch = 0;
}
else
{
_angle += 90;
if ( _angle > 270 ) _angle = 0;
}
}
if ( Input.Pressed( "Use" ) )
{
if ( ObjectToPlace is Decoration && Input.Down( "run" ) )
{
_pitch -= 90;
if ( _pitch < 0 ) _pitch = 0;
}
else
{
_angle -= 90;
if ( _angle < 0 ) _angle = 270;
}
}
}
if ( trace.Hit )
{
switch ( ObjectToPlace )
{
case Animal animal:
PlacingPosition = trace.HitPosition;
animal.WorldPosition = PlacingPosition;
break;
case Building building:
var tileRange = building.GetTileRange( _angle );
var heightRange = GridManager.Instance.Terrain.GetHeightRange( tileRange );
_height = heightRange.Max;
var center = new Vector3( (tileRange.Size.x / 2f) * GridManager.GridSize, (tileRange.Size.y / 2f) * GridManager.GridSize, 0 );
var gridOffset = center - GridManager.AlignWorldToGrid( center );
PlacingPosition = building.SnapToGrid ? GridManager.AlignWorldToGrid( trace.HitPosition ) + gridOffset : trace.HitPosition;
PlacingPosition = PlacingPosition.WithZ( _height * GridManager.HeightStep );
building.WorldPosition = PlacingPosition;
building.WorldRotation = new Angles( 0, _angle, 0 );
foreach ( var renderer in building.Components.GetAll<ModelRenderer>( FindMode.EverythingInSelfAndDescendants ) )
renderer.SceneObject?.Attributes.Set( "Ghost", CanPlace() ? 1 : 2 );
break;
case PathFurniture furniture:
var tileCenter = GridManager.AlignWorldToGrid( trace.HitPosition ) + GridManager.CentreOffset;
var offset = (trace.HitPosition - tileCenter).WithZ( 0 );
if ( MathF.Abs( offset.x ) > MathF.Abs( offset.y ) )
{
PlacingEdge = offset.x > 0 ? TileEdge.Right : TileEdge.Left;
}
else
{
PlacingEdge = offset.y > 0 ? TileEdge.Up : TileEdge.Down;
}
var direction = PlacingEdge.GetDirection();
PlacingPosition = tileCenter + new Vector3( direction * GridManager.CentreOffset * PathFurniture.GetPathOffset( furniture.FurnitureType ) );
_angle = (MathF.Atan2( direction.y, direction.x ).RadianToDegree()).RoundToInt();
furniture.WorldPosition = PlacingPosition;
furniture.WorldRotation = new Angles( 0, _angle, 0 );
foreach ( var renderer in furniture.Components.GetAll<ModelRenderer>( FindMode.EverythingInSelfAndDescendants ) )
renderer.SceneObject?.Attributes.Set( "Ghost", CanPlace() ? 1 : 2 );
break;
case Decoration decoration:
PlacingPosition = trace.HitPosition;
if ( Input.Down( "run" ) )
{
DecorationHeightOffset += Input.MouseWheel.y * 8.0f;
if ( !_heightTool.IsValid() )
_heightTool = HeightTool.Clone();
else
{
var tool = _heightTool.GetComponent<HeightTool>();
tool.UpdateStartPos( PlacingPosition );
tool.UpdateEndPos( PlacingPosition + Vector3.Up * DecorationHeightOffset );
tool.UpdateSprite( DecorationHeightOffset );
}
}
else if ( _heightTool.IsValid() )
{
_heightTool.Destroy();
_heightTool = null;
}
DecorationHeightOffset = DecorationHeightOffset.SnapToGrid( 8f );
PlacingPosition = PlacingPosition.WithZ( PlacingPosition.z + DecorationHeightOffset );
// Inverse grid snapping logic
bool shouldSnap = decoration.SnapToGrid != Input.Down( "duck" );
if ( shouldSnap )
{
PlacingPosition = trace.HitPosition.SnapToGrid( GridManager.GridSize ) - GridManager.GridSize * 0.5f + Vector3.Up * GridManager.HeightStep;
decoration.WorldPosition = PlacingPosition.WithZ( PlacingPosition.z + DecorationHeightOffset );
}
else
{
decoration.WorldPosition = PlacingPosition;
}
decoration.WorldRotation = new Angles( _pitch, _angle, 0 );
foreach ( var renderer in decoration.Components.GetAll<ModelRenderer>( FindMode.EverythingInSelfAndDescendants ) )
renderer.SceneObject?.Attributes.Set( "Ghost", 1 );
break;
}
}
if ( Input.Pressed( "Attack1" ) )
{
if ( !BuildingZone.Instance.IsOwned( PlacingPosition ) )
{
Sound.Play( "creature_error_01" );
DebugOverlay.ScreenText( Screen.Size / 2, "You don't own this land!", 14, TextFlag.CenterBottom, Color.Red, duration: 1 );
return;
}
switch ( ObjectToPlace )
{
case Animal:
TryPlaceAnimal();
break;
case Building:
TryPlace();
break;
case PathFurniture:
TryPlaceFurniture();
break;
case Decoration:
TryPlaceDecoration();
break;
}
}
}
private bool CanRotate => PlacingResource is not RideEntranceExit;
void TryPlaceAnimal()
{
if ( !CanPlace() ) return;
var root = PlacingResource.GameObject.Root ?? PlacingResource.GameObject;
var path = root.GetPrefabResourcePath();
ParkManager.Instance?.PlacePlaceable( path, ObjectToPlace.WorldPosition, _angle, _height );
}
void TryPlace()
{
if ( !CanPlace() ) return;
var root = PlacingResource.GameObject.Root ?? PlacingResource.GameObject;
var path = root.GetPrefabResourcePath();
ParkManager.Instance?.PlaceBuilding( path, ObjectToPlace.WorldPosition, _angle, _height );
}
private Vector3Int _lastAutoRotatePos;
void TryAutoRotate()
{
if ( !ObjectToPlace.IsValid() ) return;
var gridPos = CurrentGridPosition;
if ( gridPos == _lastAutoRotatePos ) return;
_lastAutoRotatePos = gridPos;
if ( ObjectToPlace is RideEntranceExit )
{
if ( !RideContext.IsValid() ) return;
var match = RideContext.ValidEntranceExitPositions
.FirstOrDefault( x => x.GridPos == gridPos );
if ( match.GridPos == gridPos )
{
var dir = match.Edge.GetDirection();
_angle = (int)MathF.Round( Rotation.LookAt( -new Vector3( dir.x, dir.y ) ).Yaw() );
}
return;
}
// Kinda sucky
if ( ObjectToPlace is Building building && building is { BuildingType: BuildingType.Shop, PathMask: var mask } )
{
if ( mask == 0 ) return;
var grid = GridManager.Instance;
PathMask pathMask = 0;
foreach ( var edge in GridManager.AllEdges )
{
var dir = edge.GetDirection();
var neighborPos = new Vector2Int( gridPos.x, gridPos.y ) + dir;
var hasPath = grid.GetCell( neighborPos )?
.GetComponents<Path>()
.Any( x => x is not Queue ) ?? false;
if ( hasPath )
{
pathMask |= edge.RotateDegrees( _angle ).ToPathMask();
}
}
if ( pathMask == 0 ) return;
var attempts = 0;
while ( (pathMask & mask) == 0 && attempts++ < 4 )
{
_angle += 90;
pathMask = pathMask.Rotate90DegreesClockwise();
}
return;
}
}
void TryPlaceFurniture()
{
if ( !ObjectToPlace.IsValid() ) return;
if ( ObjectToPlace is not PathFurniture ) return;
if ( !CanPlace() ) return;
var gridPos = GridManager.WorldToGridPosition3D( PlacingPosition );
ParkManager.Instance?.PlaceFurniture( ObjectToPlace.PrefabSource, gridPos, PlacingEdge );
}
void TryPlaceDecoration()
{
if ( !ObjectToPlace.IsValid() ) return;
ParkManager.Instance?.PlaceDecoration(
ObjectToPlace.PrefabSource,
ObjectToPlace.WorldPosition,
_angle, _pitch, _height,
_decorationColor,
RedChannel, GreenChannel, BlueChannel, AlphaChannel
);
}
public bool CanPlace()
{
if ( !IsPlacing ) return false;
if ( (ParkManager.Instance?.Money ?? 0) < PlacingResource.Cost ) return false;
var gridManager = GridManager.Instance;
if ( gridManager is null ) return false;
if ( ObjectToPlace is Building building )
{
var range = building.GetTileRange( _angle );
if ( GridManager.Instance.IsConstructionBlocked( range.Position, range.Position + range.Size - 1 ) )
return false;
if ( building is RideEntranceExit )
{
return IsValidRideEntranceExitPlacement();
}
}
else if ( ObjectToPlace is PathFurniture furniture )
{
var gridPos = GridManager.WorldToGridPosition3D( ObjectToPlace.WorldPosition );
var cell = gridManager.GetCell( new Vector2Int( gridPos ) );
return cell?.GetComponents<Path>( gridPos.z ).Any( path => furniture.IsAllowed( path, PlacingEdge ) ) ?? false;
}
return true;
}
private Vector3Int CurrentGridPosition
{
get
{
if ( !ObjectToPlace.IsValid() ) return default;
var building = ObjectToPlace as Building;
if ( !building.IsValid() ) return default;
var gridPos2d = building.GetTileRange( _angle ).Position;
var height = GridManager.RoundWorldHeightToGrid( ObjectToPlace.WorldPosition.z );
return new Vector3Int( gridPos2d.x, gridPos2d.y, height );
}
}
private bool IsValidRideEntranceExitPlacement()
{
if ( !RideContext.IsValid() ) return false;
var gridPos = CurrentGridPosition;
var match = RideContext.ValidEntranceExitPositions.FirstOrDefault( x => x.GridPos == gridPos );
if ( match.GridPos != gridPos ) return false;
var dir = match.Edge.GetDirection();
var expectedAngle = (int)MathF.Round( Rotation.LookAt( -new Vector3( dir.x, dir.y ) ).Yaw() );
return _angle == expectedAngle;
}
public void StartPlacingEntrance( BasicRide ride )
{
StartPlacing( EntrancePrefab.GetComponent<Building>() );
RideContext = ride;
}
public void StartPlacingExit( BasicRide ride )
{
StartPlacing( ExitPrefab.GetComponent<Building>() );
RideContext = ride;
}
public void SetDecorationColor( Color color, TintChannel channel )
{
if ( ObjectToPlace.IsValid() )
{
var tintComponent = ObjectToPlace.GetTintComponent();
if ( !tintComponent.IsValid() )
return;
switch ( channel )
{
case TintChannel.Primary:
RedChannel = color;
break;
case TintChannel.Secondary:
GreenChannel = color;
break;
case TintChannel.Accent:
BlueChannel = color;
break;
case TintChannel.Detail:
AlphaChannel = color;
break;
}
}
}
/// <summary>
/// A building was placed by someone.
/// </summary>
public void OnBuildingPlaced( Building building )
{
Stats.Increment( $"building.placed" );
Stats.Increment( $"building.placed.{building.Title.ToIdentifier()}" );
switch ( building )
{
case TrackRide trackRide:
StopPlacing();
TrackBuilder.Instance?.StartBuilding( trackRide );
break;
case BasicRide ride:
StopPlacing();
SelectionSystem.Instance.Select( building.GameObject );
StartPlacingEntrance( ride );
return;
case RideEntranceExit { IsExit: false }:
if ( RideContext.IsValid() )
{
if ( RideContext.Entrance.IsValid() )
{
RideContext.Entrance.DestroyGameObject();
}
if ( !RideContext.HasExit )
{
StartPlacingExit( RideContext );
return;
}
}
break;
case RideEntranceExit { IsExit: true }:
if ( RideContext.IsValid() && RideContext.Exit.IsValid() )
{
RideContext.Exit.DestroyGameObject();
}
break;
}
StopPlacing();
}
}