Player/ItemPlacer.cs
using System;
using Clover.Data;
using Clover.Inventory;
using Clover.Items;
using Clover.Persistence;
using Clover.Ui;
using Clover.WorldBuilder;
namespace Clover.Player;
[Title( "Item Placer" )]
[Icon( "inventory" )]
[Category( "Clover/Player" )]
public class ItemPlacer : Component, IWorldEvent
{
[ConVar( "clover_itemplacer_position_snap_enabled", Saved = true )]
public static bool PositionSnapEnabled { get; set; } = false;
[ConVar( "clover_itemplacer_position_snap_distance", Saved = true )]
public static float PositionSnapDistance { get; set; } = 16f;
public readonly float[] PositionSnapDistances = { 1, 2, 4, 8, 16, 32, 64, 128, 256, 512 };
[ConVar( "clover_itemplacer_show_grid", Saved = true )]
public static bool ShowGrid { get; set; } = true;
[ConVar( "clover_itemplacer_rotation_snap_distance", Saved = true )]
public static float RotationSnapDistance { get; set; } = 15f;
public readonly float[] RotationSnapDistances = { 1, 5, 15, 30, 45, 90 };
private PlayerCharacter Player => GetComponent<PlayerCharacter>();
[Property] public Model CursorModel { get; set; }
[Property] public SoundEvent StartPlacingSound { get; set; }
[Property] public SoundEvent StopPlacingSound { get; set; }
[Property] public SoundEvent RotateSound { get; set; }
public bool IsPlacing;
public bool IsMoving;
public int InventorySlotIndex;
public InventorySlot InventorySlot =>
Player.Inventory.Container.GetSlotByIndex( InventorySlotIndex );
private ItemData ItemData => InventorySlot.GetItem().ItemData;
private GameObject _ghost;
private ItemData _selectedItemData;
// private bool _isPlacingFromInventory;
private bool _isAdjustingCamera;
private Vector2 _cameraAdjustmentStart;
public WorldItem CurrentPlacedItem { get; set; }
private TimeSince _lastAction;
private const float MaxStartMoveDistance = 150f;
private const float MaxMoveDistance = 200f;
protected override void OnStart()
{
Mouse.Visible = true;
_lastAction = 0;
}
public void StartMovingPlacedItem( WorldItem selectedGameObject )
{
if ( !selectedGameObject.IsValid() )
{
return;
}
if ( IsPlacing )
{
StopPlacing();
}
// _isPlacingFromInventory = false;
IsMoving = true;
_selectedItemData = selectedGameObject.ItemData;
// selectedGameObject.DestroyGameObject();
CurrentPlacedItem = selectedGameObject;
var clone = selectedGameObject.GameObject.Clone( selectedGameObject.WorldPosition,
selectedGameObject.WorldRotation );
PlaceGhostInternal( clone );
// TODO: hide worlditem
CurrentPlacedItem.Hide();
Mouse.Visible = true;
Mouse.Position = Scene.Camera.PointToScreenPixels( selectedGameObject.WorldPosition );
Sound.Play( StartPlacingSound );
}
void IWorldEvent.OnWorldChanged( World world )
{
StopPlacing();
StopMoving();
}
public void StartPlacingInventorySlot( int inventorySlotIndex )
{
if ( !Player.IsValid() ) throw new System.Exception( "Player is not valid" );
if ( !Player.Inventory.IsValid() ) throw new System.Exception( "Player Inventory is not valid" );
if ( Player.Inventory.Container == null )
throw new System.Exception( "Player Inventory Container is not valid" );
if ( Player.World.Data.DisableItemPlacement )
{
Player.Notify( Notifications.NotificationType.Error, "Item placement is disabled in this area" );
return;
}
if ( IsPlacing )
{
StopPlacing();
}
// _isPlacingFromInventory = true;
InventorySlotIndex = inventorySlotIndex;
IsPlacing = true;
CreateGhostFromInventory();
Mouse.Visible = true;
Sound.Play( StartPlacingSound );
}
public void StopPlacing()
{
if ( IsMoving || IsPlacing )
{
Sound.Play( StopPlacingSound );
}
IsMoving = false;
IsPlacing = false;
DestroyGhost();
_selectedItemData = null;
_isAdjustingCamera = false;
_heightMode = false;
// Mouse.Visible = false;
}
public void StopMoving()
{
if ( IsMoving || IsPlacing )
{
Sound.Play( StopPlacingSound );
}
IsMoving = false;
IsPlacing = false;
DestroyGhost();
_selectedItemData = null;
if ( CurrentPlacedItem.IsValid() )
{
CurrentPlacedItem.Show();
}
CurrentPlacedItem = null;
_isAdjustingCamera = false;
_heightMode = false;
// Mouse.Visible = false;
}
public void CreateGhostFromInventory()
{
var item = InventorySlot.GetItem();
_selectedItemData = item.ItemData;
var gameObject = item.ItemData.PlaceScene.Clone( Player.WorldPosition,
Rotation.FromYaw( Player.PlayerController.Yaw ).Angles().SnapToGrid( RotationSnapDistance ) );
PlaceGhostInternal( gameObject );
}
private void PlaceGhostInternal( GameObject clonedGameObject )
{
clonedGameObject.NetworkMode = NetworkMode.Never;
// kill all colliders
foreach ( var collider in clonedGameObject.Components
.GetAll<Collider>( FindMode.EverythingInSelfAndDescendants )
.ToList() )
{
if ( collider is BoxCollider boxCollider )
{
_colliderSize = boxCollider.Scale;
_colliderCenter = boxCollider.Center;
}
else if ( collider is HullCollider hullCollider )
{
_colliderSize = hullCollider.BoxSize;
_colliderCenter = hullCollider.Center;
}
collider.Destroy();
}
// kill all worlditems
foreach ( var worldItem in clonedGameObject.Components
.GetAll<WorldItem>( FindMode.EverythingInSelfAndDescendants )
.ToList() )
{
worldItem.Destroy();
}
// tint the ghost
foreach ( var renderable in clonedGameObject.Components
.GetAll<ModelRenderer>( FindMode.EverythingInSelfAndDescendants ).ToList() )
{
renderable.Tint = renderable.Tint.WithAlpha( 0.5f );
}
_ghost = clonedGameObject;
// create cursor
/*cursor = Scene.CreateObject();
cursor.NetworkMode = NetworkMode.Never;
var model = cursor.AddComponent<ModelRenderer>();
model.Tint = Color.Parse( "#FFFFFF2B" ) ?? Color.White;
model.Model = CursorModel;
model.RenderType = ModelRenderer.ShadowRenderType.Off;
cursor.WorldScale = new Vector3( 1, 1, 0.3f );*/
}
public void DestroyGhost()
{
if ( _ghost.IsValid() )
{
_ghost.Destroy();
}
_ghost = null;
/*if ( cursor.IsValid() )
{
cursor.Destroy();
}
cursor = null;*/
}
protected override void OnDestroy()
{
base.OnDestroy();
DestroyGhost();
}
/*protected override void OnFixedUpdate()
{
if ( IsProxy ) return;
if ( !IsPlacing && !IsMoving ) return;
if ( !_ghost.IsValid() ) return;
// CheckInput();
// UpdateGhostTransform();
// UpdateVisuals();
}*/
private bool _heightMode;
private Vector3 _heightModeStart;
private void CheckInput()
{
if ( Input.Pressed( "ItemPlacerConfirm" ) )
{
if ( _isValidPlacement )
{
if ( IsPlacing )
{
PlaceItem();
}
else if ( IsMoving )
{
MoveItem();
}
}
else
{
Log.Info( "Bad" );
Player.Notify( Notifications.NotificationType.Warning, "Invalid placement" );
}
Input.ReleaseAction( "ItemPlacerConfirm" );
Input.Clear( "ItemPlacerConfirm" );
_lastAction = 0;
return;
}
if ( Input.Pressed( "ItemPlacerCancel" ) )
{
if ( IsPlacing )
{
StopPlacing();
}
else if ( IsMoving )
{
StopMoving();
}
return;
}
if ( Input.Pressed( "PickUp" ) && IsMoving )
{
PickUpMovingItem();
return;
}
if ( Input.Pressed( "ItemPlacerHeight" ) )
{
Log.Info( "Height mode on" );
_heightMode = true;
_heightModeStart = _ghost.WorldPosition;
_cameraAdjustmentStart = Mouse.Position;
}
if ( Input.Released( "ItemPlacerHeight" ) )
{
Log.Info( "Height mode off" );
_heightMode = false;
}
if ( Input.Pressed( "RotateClockwise" ) )
{
_ghost.WorldRotation *= Rotation.FromYaw( -RotationSnapDistance );
_ghost.WorldRotation = _ghost.WorldRotation.Angles().SnapToGrid( RotationSnapDistance );
Sound.Play( RotateSound );
}
else if ( Input.Pressed( "RotateCounterClockwise" ) )
{
_ghost.WorldRotation *= Rotation.FromYaw( RotationSnapDistance );
_ghost.WorldRotation = _ghost.WorldRotation.Angles().SnapToGrid( RotationSnapDistance );
Sound.Play( RotateSound );
}
if ( Input.Pressed( "CameraAdjust" ) )
{
_isAdjustingCamera = true;
_cameraAdjustmentStart = Mouse.Position;
}
if ( Input.Released( "CameraAdjust" ) )
{
_isAdjustingCamera = false;
}
if ( _isAdjustingCamera )
{
if ( Mouse.Position.x > _cameraAdjustmentStart.x + 30f )
{
Player.CameraController.RotateCamera( Rotation.FromYaw( -CameraController.CameraRotateSnapDistance ) );
_cameraAdjustmentStart = Mouse.Position;
}
else if ( Mouse.Position.x < _cameraAdjustmentStart.x - 30f )
{
Player.CameraController.RotateCamera( Rotation.FromYaw( CameraController.CameraRotateSnapDistance ) );
_cameraAdjustmentStart = Mouse.Position;
}
}
if ( Input.Pressed( "ItemPlacerPositionSnapDecrease" ) )
{
var index = Array.IndexOf( PositionSnapDistances, PositionSnapDistance );
if ( index > 0 )
{
PositionSnapDistance = PositionSnapDistances[index - 1];
Sound.Play( RotateSound );
}
}
else if ( Input.Pressed( "ItemPlacerPositionSnapIncrease" ) )
{
var index = Array.IndexOf( PositionSnapDistances, PositionSnapDistance );
if ( index < 0 )
{
PositionSnapDistance = PositionSnapDistances[0];
Sound.Play( RotateSound );
}
else if ( index < PositionSnapDistances.Length - 1 )
{
PositionSnapDistance = PositionSnapDistances[index + 1];
Sound.Play( RotateSound );
}
}
else if ( Input.Pressed( "ItemPlacerRotationSnapDecrease" ) )
{
var index = Array.IndexOf( RotationSnapDistances, RotationSnapDistance );
if ( index > 0 )
{
RotationSnapDistance = RotationSnapDistances[index - 1];
Sound.Play( RotateSound );
}
}
else if ( Input.Pressed( "ItemPlacerRotationSnapIncrease" ) )
{
var index = Array.IndexOf( RotationSnapDistances, RotationSnapDistance );
if ( index < RotationSnapDistances.Length - 1 )
{
RotationSnapDistance = RotationSnapDistances[index + 1];
Sound.Play( RotateSound );
}
}
}
private void PickUpMovingItem()
{
if ( !CurrentPlacedItem.CanPickup( Player ) ) return;
CurrentPlacedItem.OnPickup( Player );
StopMoving();
}
public WorldItem CurrentHoveredItem { get; set; }
private void CheckStartMove()
{
CurrentHoveredItem = null;
var trace = Scene.Trace.Ray( Scene.Camera.ScreenPixelToRay( Mouse.Position ), 1000f )
.WithoutTags( "player", "invisiblewall", "doorway", "stairs", "room_invisible" )
.Run();
if ( !trace.Hit ) return;
var worldItem = trace.GameObject.GetComponent<WorldItem>();
if ( !worldItem.IsValid() ) return;
if ( !worldItem.CanPickup( Player ) ) return;
if ( !worldItem.ItemData.CanPlace ) return;
var tooFarAway = worldItem.WorldPosition.Distance( Player.WorldPosition ) > MaxStartMoveDistance;
if ( !tooFarAway )
{
worldItem.ItemHighlight.Enabled = true;
CurrentHoveredItem = worldItem;
}
if ( Input.Released( "ItemPlacerConfirm" ) && _lastAction > 0.2f )
{
if ( tooFarAway )
{
Player.Notify( Notifications.NotificationType.Warning, $"{worldItem.ItemData.Name} is too far away" );
return;
}
StartMovingPlacedItem( worldItem );
/*_clickedWorldItem = worldItem;
_clickedWorldItemScreenPosition = Mouse.Position;*/
}
/*else if ( Input.Released( "ItemPlacerConfirm" ) )
{
_clickedWorldItem = null;
}
if ( Input.Down( "ItemPlacerConfirm" ) && _clickedWorldItem.IsValid() )
{
if ( tooFarAway )
{
Player.Notify( Notifications.NotificationType.Warning, "Too far away" );
return;
}
var distance = Mouse.Position.Distance( _clickedWorldItemScreenPosition );
if ( distance > 10f )
{
_clickedWorldItem.ItemHighlight.Enabled = false;
StartMovingPlacedItem( _clickedWorldItem );
_clickedWorldItem = null;
}
}*/
}
private void PlaceItem()
{
try
{
Player.World.SpawnPlacedItem( InventorySlot.GetItem(), _ghost.WorldPosition, _ghost.WorldRotation );
}
catch ( Exception e )
{
// Log.Error( e );
Player.Notify( Notifications.NotificationType.Error, e.Message );
return;
}
InventorySlot.TakeOneOrDelete();
StopPlacing();
}
private void MoveItem()
{
if ( !CurrentPlacedItem.IsValid() )
{
Log.Error( "CurrentPlacedItem is not valid" );
StopMoving();
return;
}
if ( _ghost.WorldPosition.Distance( Player.WorldPosition ) > MaxMoveDistance )
{
Player.Notify( Notifications.NotificationType.Warning, "Item placed too far away" );
// StopMoving();
return;
}
CurrentPlacedItem.WorldPosition = _ghost.WorldPosition;
CurrentPlacedItem.WorldRotation = _ghost.WorldRotation;
CurrentPlacedItem.Transform.ClearInterpolation();
StopMoving();
Log.Info( "Moved item" );
}
private Material _invalidMaterial = Material.Load( "materials/ghost_invalid.vmat" );
private void SetGhostTint()
{
if ( !_ghost.IsValid() ) return;
var s = MathF.Sin( Time.Now * 5 ) * 0.4f;
foreach ( var renderable in _ghost.Components.GetAll<ModelRenderer>( FindMode.EverythingInSelfAndDescendants )
.ToList() )
{
renderable.Tint = _isValidPlacement ? Color.White.WithAlpha( 0.5f + s ) : Color.Red.WithAlpha( 0.5f + s );
renderable.MaterialOverride = _isValidPlacement ? null : _invalidMaterial;
}
}
/*private void SetCursorTintColor( Color color )
{
var renderable = cursor.GetComponent<ModelRenderer>();
renderable.SceneObject.Attributes.Set( "tint", color.WithAlpha( 1 ) );
renderable.SceneObject.Attributes.Set( "opacity", color.a );
}*/
private void UpdateVisuals()
{
SetGhostTint();
// SetCursorTintColor( _isValidPlacement ? Color.White.WithAlpha( 0.2f + s ) : Color.Red.WithAlpha( 0.2f + s ) );
}
private bool _isValidPlacement;
private Vector3 _colliderSize;
private Vector3 _colliderCenter;
private void UpdateGhostTransform()
{
if ( !_ghost.IsValid() ) return;
if ( _colliderSize == Vector3.Zero )
{
Log.Error( "Collider size is zero" );
return;
}
var ray = Scene.Camera.ScreenPixelToRay( Mouse.Position );
var box = BBox.FromPositionAndSize( _colliderCenter, _colliderSize );
box = box.Rotate( _ghost.WorldRotation );
var trace = Scene.Trace.Box( box, ray, 1000f )
.WithoutTags( "player", "invisiblewall", "doorway", "stairs", "room_invisible", "invisible" )
.Run();
if ( !trace.Hit )
{
_isValidPlacement = false;
return;
}
if ( trace.GameObject.Tags.Has( "noplace" ) )
{
_isValidPlacement = false;
return;
}
var interiorManager = Player.World.Components.Get<InteriorManager>( FindMode.InDescendants );
if ( interiorManager != null && !interiorManager.IsInCurrentRoom( trace.EndPosition ) )
{
_isValidPlacement = false;
return;
}
var endPosition = trace.EndPosition;
if ( PositionSnapEnabled && !Input.Down( "Snap" ) )
{
endPosition = endPosition.SnapToGrid( PositionSnapDistance );
}
else if ( !PositionSnapEnabled && Input.Down( "Snap" ) )
{
endPosition = endPosition.SnapToGrid( PositionSnapDistance );
}
endPosition += _selectedItemData.PlaceModeOffset;
if ( _heightMode )
{
var delta = Mouse.Position - _cameraAdjustmentStart;
endPosition = (_heightModeStart + new Vector3( 0, 0, -(delta.y / 5f) )).SnapToGrid( PositionSnapDistance );
}
// var gridPosition = Player.World.WorldToItemGrid( endPosition );
// TODO: Check if the item can be placed here
_isValidPlacement = !Player.World.IsPositionOccupied( endPosition, CurrentPlacedItem?.GameObject, 4f ) &&
!Player.World.IsNearPlayer( endPosition );
_ghost.WorldPosition = endPosition;
}
protected override void OnUpdate()
{
if ( IsProxy ) return;
if ( !IsPlacing && !IsMoving )
{
CheckStartMove();
return;
}
if ( !Player.IsValid() ) return;
if ( !_ghost.IsValid() ) return;
if ( !Scene.IsValid() ) return;
/*if ( IsPlacing && ShowGrid )
{
// TODO: re-add grid when it can be positioned relative to the player
// Gizmo.Transform = new Transform( Player.WorldPosition );
// Gizmo.Draw.Grid( Gizmo.GridAxis.XY, new Vector2( 32f, 32f ) );
}
*/
CheckInput();
UpdateGhostTransform();
UpdateVisuals();
if ( !_ghost.IsValid() ) return;
var trace = Scene.Trace.Ray( _ghost.WorldPosition, _ghost.WorldPosition + Vector3.Down * 300f )
.WithoutTags( "player", "invisiblewall", "doorway", "stairs", "room_invisible" )
.Run();
if ( trace.Hit )
{
var endPos = trace.EndPosition;
Gizmo.Draw.Color = Color.Yellow;
Gizmo.Draw.Arrow( _ghost.WorldPosition, endPos );
Gizmo.Draw.Color = Color.White;
Gizmo.Draw.Text( Math.Round( trace.Distance ).ToString(), new Transform( endPos + Vector3.Right * 16 ),
"Roboto", 24 );
}
var bbox1 = BBox.FromPositionAndSize( _colliderCenter, _colliderSize );
bbox1 = bbox1.Rotate( _ghost.WorldRotation );
bbox1 = bbox1.Translate( _ghost.WorldPosition );
Gizmo.Draw.LineBBox( bbox1 );
foreach ( var worldItem in Scene.GetAllComponents<WorldItem>() )
{
if ( worldItem == CurrentPlacedItem ) continue;
if ( worldItem == CurrentHoveredItem ) continue;
var collider = worldItem.GameObject.GetComponent<BoxCollider>();
if ( collider == null ) continue;
var bbox = BBox.FromPositionAndSize( collider.Center, collider.Scale )
.Rotate( worldItem.WorldRotation )
.Translate( worldItem.WorldPosition );
Gizmo.Draw.LineBBox( bbox );
}
}
}