Data/ItemData.cs
using System;
using Clover.Carriable;
using Clover.Components;
using Clover.Inventory;
using Clover.Items;
using Clover.Persistence;
using Sandbox.Utility;
namespace Clover.Data;
[AssetType( Name = "Item", Extension = "item" )]
[Category( "Clover/Items" )]
[Icon( "weekend" )]
// [JsonPolymorphic]
// [JsonDerivedType( typeof(ItemData), "item" )]
// [JsonDerivedType( typeof(ToolData), "tool" )]
public class ItemData : GameResource
{
/// <summary>
/// Unique identifier for this item.
/// </summary>
[Property]
public string Id { get; set; }
[Button( "Generate ID" )]
public void GenerateId()
{
Id = $"{ResourceName}:{Crc64.FromString( ResourcePath )}";
}
[Property] public string Name { get; set; }
[Property, TextArea] public string Description { get; set; }
[Property, Group( "World" )] public int Width { get; set; } = 1;
[Property, Group( "World" )] public int Height { get; set; } = 1;
// [Property, Group( "World" )] // default to floor and underground
// public World.ItemPlacement Placements { get; set; } = World.ItemPlacement.Floor | World.ItemPlacement.Underground;
[Property, Group( "World" )] public bool CanBeBuried { get; set; } = true;
[Property, Group( "Inventory" )] public bool CanDrop { get; set; } = true;
[Property, Group( "Inventory" )] public bool CanPlace { get; set; } = true;
[Property, Group( "Inventory" )] public bool DisablePickup { get; set; } = false;
[Property, Group( "Inventory" )] public bool IsStackable { get; set; } = false;
[Property, ShowIf( nameof(IsStackable), true )]
public int StackSize { get; set; } = 1;
[Property, Group( "Scenes" )] public GameObject ModelScene { get; set; }
[Property, Group( "Scenes" )] public GameObject DropScene { get; set; }
[Property, Group( "Scenes" )] public GameObject PlaceScene { get; set; }
[Property, ReadOnly, Group( "Scenes" )]
public virtual GameObject DefaultTypeScene => PlaceScene;
[Property, ImageAssetPath] public string Icon { get; set; }
[Property, Group( "Object" )] // TODO: move this to yet another class?
[Description( "Custom world object to spawn. Don't use this unless you know what you're doing." )]
public ObjectData ObjectData { get; set; }
public bool HideInSpawnMenu { get; set; }
public Vector3 PlaceModeOffset { get; set; }
[Hide]
public delegate int GetPriceDelegate( DateTime dateTime );
[Property, Group( "Shop" )] public int BaseBuyPrice { get; set; } = 0;
[Property, Group( "Shop" )] public bool CanSell { get; set; } = true;
[Property, Group( "Shop" )] public int BaseSellPrice { get; set; } = 0;
[Property, Group( "Shop" )] public bool CanBuy { get; set; } = true;
[Property, Group( "Shop" )] public GetPriceDelegate GetCustomSellPrice { get; set; }
[Property, Group( "Shop" )] public GetPriceDelegate GetCustomBuyPrice { get; set; }
[Property, Group( "Persistence" ), TargetType( typeof(PersistentItem) )]
public Type PersistentType { get; set; } = typeof(PersistentItem);
public static T GetById<T>( string id ) where T : ItemData
{
return ResourceLibrary.GetAll<T>()
.FirstOrDefault( i => i.ResourceName == id || i.ResourcePath == id || i.Id == id ) ??
throw new Exception( $"Item data not found: {id}" );
}
public virtual string GetIcon()
{
return Icon ?? "ui/icons/default_item.png";
}
public virtual Texture GetIconTexture()
{
return Texture.LoadFromFileSystem( GetIcon(), FileSystem.Mounted );
}
/// <summary>
/// Returns a list of grid positions for this item.
/// </summary>
/// <param name="itemRotation">Amount of rotation to apply to the item.</param>
/// <param name="originOffset">Offset to apply to the item.</param>
/// <returns></returns>
/// <exception cref="Exception"></exception>
public List<Vector2Int> GetGridPositions( World.ItemRotation itemRotation, Vector2Int originOffset = default )
{
var positions = new List<Vector2Int>();
if ( Width == 0 || Height == 0 ) throw new Exception( "Item has no size" );
if ( Width == 1 && Height == 1 )
{
return
new List<Vector2Int>
{
originOffset
}; // if the item is 1x1, return the origin since it's the only position
}
if ( itemRotation == World.ItemRotation.North )
{
for ( var x = 0; x < Width; x++ )
{
for ( var y = 0; y < Height; y++ )
{
positions.Add( new Vector2Int( originOffset.x + x, originOffset.y + y ) );
}
}
}
else if ( itemRotation == World.ItemRotation.South )
{
for ( var x = 0; x < Width; x++ )
{
for ( var y = 0; y < Height; y++ )
{
positions.Add( new Vector2Int( originOffset.x + x, originOffset.y - y ) );
}
}
}
else if ( itemRotation == World.ItemRotation.East )
{
for ( var x = 0; x < Height; x++ )
{
for ( var y = 0; y < Width; y++ )
{
positions.Add( new Vector2Int( originOffset.x + x, originOffset.y + y ) );
}
}
}
else if ( itemRotation == World.ItemRotation.West )
{
for ( var x = 0; x < Height; x++ )
{
for ( var y = 0; y < Width; y++ )
{
positions.Add( new Vector2Int( originOffset.x - x, originOffset.y + y ) );
}
}
}
return positions;
}
public Vector2Int GetBounds( World.ItemRotation itemRotation )
{
if ( itemRotation == World.ItemRotation.North || itemRotation == World.ItemRotation.South )
{
return new Vector2Int( Width, Height );
}
else
{
return new Vector2Int( Height, Width );
}
}
public int GetMaxBounds()
{
return Math.Max( Width, Height );
}
public bool IsSameAs( ItemData item )
{
return item != null && (item.ResourcePath == ResourcePath || item.GetIdentifier() == GetIdentifier());
}
public GameObject SpawnPlaced()
{
return Spawn( PlaceScene );
}
public GameObject SpawnDropped()
{
return Spawn( DropScene );
}
private GameObject Spawn( GameObject scene )
{
if ( !scene.IsValid() ) return null;
return scene.Clone();
}
// TODO: why both ItemAction and ContextMenuItem?
public struct ItemAction
{
public string Name;
public string Icon;
public string Image;
public Action Action;
}
public virtual IEnumerable<ItemAction> GetActions( InventorySlot slot )
{
// yield break;
if ( ObjectData.IsValid() )
{
yield return new ItemAction
{
Name = "Spawn",
Icon = "add",
Action = () =>
{
slot.SpawnObject();
}
};
}
var player = slot.InventoryContainer.Player;
if ( player != null )
{
if ( slot.GetItem().ItemData.CanBeBuried )
{
var shovel = player.Equips.GetEquippedItem<Shovel>( Equips.EquipSlot.Tool );
if ( shovel != null )
{
var hole = player.World.GetWorldItem<Hole>( player.GetAimingGridPosition() );
if ( hole != null )
{
yield return new ItemAction
{
Name = "Bury",
Icon = "file_upload",
Action = () =>
{
shovel.BuryItem( slot, hole );
shovel.GameObject.PlaySound( shovel.FillSound );
slot.TakeOneOrDelete();
}
};
}
else
{
// Log.Warning( "No hole found" );
}
}
else
{
// Log.Warning( "No shovel equipped" );
}
}
else
{
// Log.Warning( "Item does not support underground placement" );
}
}
else
{
// Log.Warning( "Player is null" );
}
}
public static ItemData Get( string id )
{
var itemData = ResourceLibrary.GetAll<ItemData>()
.FirstOrDefault( x => x.Id == id || x.ResourceName == id || x.ResourcePath == id );
if ( itemData == null )
{
Log.Error( $"Item data not found: {id}" );
}
return itemData;
}
public PersistentItem CreatePersistentItem()
{
return new PersistentItem { ItemId = GetIdentifier(), };
}
/*protected override void PostLoad()
{
base.PostLoad();
if ( string.IsNullOrEmpty( Id ) )
{
// create id based on path
// TODO: is this a good idea?
Log.Warning( $"Item {ResourcePath} has no id, generating one" );
// Id = $"{ResourceName}:{Crc64.FromString( ResourcePath )}";
}
}*/
public string GetIdentifier()
{
return Id;
}
public virtual void OnPersistentItemInitialize( PersistentItem persistentItem )
{
}
}