using UnityEngine; using Ashwild.Inventory; namespace Ashwild.Building { /// /// What a buildable is allowed to rest on. This is the one placement rule that genuinely differs /// per structure — a foundation sits on terrain, a wall or a ceiling only makes sense attached to /// something — so it is authored here rather than configured globally on BuildManager (which keeps /// owning the project-wide layer masks: what blocks a build does not vary per build). /// /// is deliberately the zero value: Unity deserializes a missing field to /// 0, so buildables authored before this rule existed keep their old, permissive behaviour instead /// of silently becoming unplaceable. /// public enum PlacementSupport { /// Free placement anywhere the aim lands, or connected to a socket. Foundations, floors. GroundOrSnap, /// Only valid while connected to a matching socket. Walls, ceilings, roofs. SnapOnly } /// /// Authoring data for one buildable structure shown in the construction menu (a wall, a /// floor, a door, ...). Mirrors the ScriptableObject-for-data / MonoBehaviour-for-logic /// split: it carries only what the menu card and the placement ghost need, no behaviour. /// The final networked spawn will map this to an id later, the same way ItemData does. /// [CreateAssetMenu(fileName = "NewBuildable", menuName = "Building/Buildable Data")] public class BuildableData : ScriptableObject { #region Serialized Fields [Header("Identity")] [Tooltip("Name shown on the menu card.")] [SerializeField] private string displayName; [Tooltip("Icon shown on the menu card.")] [SerializeField] private Sprite icon; [TextArea] [SerializeField] private string description; [Header("Prefabs")] [Tooltip("Semi-transparent preview spawned in the world while positioning the structure.")] [SerializeField] private GameObject ghostPrefab; [Tooltip("The real structure spawned once the placement is confirmed.")] [SerializeField] private GameObject builtPrefab; [Header("Cost")] [Tooltip("Resources consumed from the builder's inventory on each placement. Leave empty for a free build.")] [SerializeField] private BuildCost[] cost; [Header("Placement")] [Tooltip("Where this structure may be committed. Walls, ceilings and roofs should be Snap Only so they cannot be dropped in mid-air; foundations and floors stay Ground Or Snap.")] [SerializeField] private PlacementSupport support = PlacementSupport.GroundOrSnap; [Header("Health")] [Tooltip("Hit points the placed structure starts with. Damage subtracts from this (server-authoritative in BuildRegistry); at 0 the build is destroyed. A manual demolition refunds the cost in proportion to the health left.")] [SerializeField] private float maxHealth = 100f; #endregion #region Public API public string DisplayName => displayName; public Sprite Icon => icon; public string Description => description; public GameObject GhostPrefab => ghostPrefab; public GameObject BuiltPrefab => builtPrefab; public BuildCost[] Cost => cost; public float MaxHealth => maxHealth; public PlacementSupport Support => support; /// /// Whether the given inventory holds enough of every cost line to place this structure once. /// A null inventory or an empty cost list counts as affordable (free build). /// public bool CanAfford(PlayerInventory inventory) { if (cost == null || cost.Length == 0) return true; if (inventory == null) return false; for (int i = 0; i < cost.Length; i++) { if (cost[i].item == null) continue; if (!inventory.HasItem(cost[i].item, cost[i].quantity)) return false; } return true; } #endregion } }