Initial commit
This commit is contained in:
12
Assets/Scripts/Item.cs
Normal file
12
Assets/Scripts/Item.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using UnityEngine;
|
||||
|
||||
[System.Serializable]
|
||||
public class Item : MonoBehaviour
|
||||
{
|
||||
public static Item instance { get; private set; }
|
||||
public string itemName; // Name of the item
|
||||
public int itemPrice; // Price of the item
|
||||
public Sprite itemSprite; // Optional: Sprite for the item
|
||||
|
||||
// Optional: Add more properties as needed, such as description, rarity, etc.
|
||||
}
|
||||
2
Assets/Scripts/Item.cs.meta
Normal file
2
Assets/Scripts/Item.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 76e5542449aa29b46a79572a89069f91
|
||||
138
Assets/Scripts/ItemPickup.cs
Normal file
138
Assets/Scripts/ItemPickup.cs
Normal file
@@ -0,0 +1,138 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
public class ItemPickup : MonoBehaviour
|
||||
{
|
||||
[Header("Layer Settings")]
|
||||
public LayerMask pickupLayer;
|
||||
[Header("Input Settings")]
|
||||
public InputActionAsset inputActionAsset;
|
||||
|
||||
[Header("Pickup Settings")]
|
||||
public float pickupSpeed = 10f; // How fast it snaps to your hand
|
||||
public float holdDistance = 3f; // Default distance
|
||||
|
||||
private InputAction pickUpAction;
|
||||
private Camera mainCamera;
|
||||
|
||||
// We store the Rigidbody instead of the GameObject
|
||||
private Rigidbody currentPickedRB = null;
|
||||
|
||||
void Awake()
|
||||
{
|
||||
mainCamera = Camera.main;
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
if (inputActionAsset != null)
|
||||
{
|
||||
pickUpAction = inputActionAsset.FindAction("PickUp");
|
||||
if (pickUpAction != null)
|
||||
{
|
||||
pickUpAction.Enable();
|
||||
pickUpAction.started += OnPickUpStarted;
|
||||
pickUpAction.canceled += OnPickUpCanceled;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (pickUpAction != null)
|
||||
{
|
||||
pickUpAction.started -= OnPickUpStarted;
|
||||
pickUpAction.canceled -= OnPickUpCanceled;
|
||||
pickUpAction.Disable();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPickUpStarted(InputAction.CallbackContext context)
|
||||
{
|
||||
// 1. Raycast to find object
|
||||
Vector2 mousePos = Mouse.current.position.ReadValue();
|
||||
Ray ray = mainCamera.ScreenPointToRay(mousePos);
|
||||
RaycastHit hit;
|
||||
|
||||
if (Physics.Raycast(ray, out hit))
|
||||
{
|
||||
// Check if it has a Rigidbody and match layer
|
||||
if (hit.collider.TryGetComponent(out Rigidbody rb) &&
|
||||
((1 << hit.collider.gameObject.layer) & pickupLayer) != 0)
|
||||
{
|
||||
currentPickedRB = rb;
|
||||
|
||||
// --- CRITICAL FIX START ---
|
||||
// If we pick it off a Shelf, it is frozen (Kinematic).
|
||||
// We must unfreeze it immediately so we can apply velocity.
|
||||
currentPickedRB.isKinematic = false;
|
||||
// --- CRITICAL FIX END ---
|
||||
|
||||
// 2. Setup Physics for "Holding"
|
||||
currentPickedRB.useGravity = false;
|
||||
|
||||
// Add damping so it doesn't drift around like it's on ice
|
||||
currentPickedRB.linearDamping = 10;
|
||||
currentPickedRB.angularDamping = 5;
|
||||
|
||||
// Optional: Calculate distance to keep it where we clicked
|
||||
holdDistance = hit.distance;
|
||||
|
||||
// Notify RayCastSystem
|
||||
RayCastSystem rayCastSystem = GetComponent<RayCastSystem>();
|
||||
if (rayCastSystem != null)
|
||||
{
|
||||
rayCastSystem.SetCurrentPickedRB(rb);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPickUpCanceled(InputAction.CallbackContext context)
|
||||
{
|
||||
if (currentPickedRB != null)
|
||||
{
|
||||
// 3. Reset Physics properties
|
||||
currentPickedRB.useGravity = true;
|
||||
currentPickedRB.linearDamping = 0; // Reset to default (usually 0)
|
||||
currentPickedRB.angularDamping = 0.05f;
|
||||
|
||||
currentPickedRB = null;
|
||||
// Notify RayCastSystem that an object is no longer picked up
|
||||
RayCastSystem rayCastSystem = GetComponent<RayCastSystem>();
|
||||
if (rayCastSystem != null)
|
||||
{
|
||||
rayCastSystem.SetCurrentPickedRB(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Physics movement must be in FixedUpdate
|
||||
private void FixedUpdate()
|
||||
{
|
||||
if (currentPickedRB != null)
|
||||
{
|
||||
MoveObjectPhysics();
|
||||
}
|
||||
}
|
||||
|
||||
private void MoveObjectPhysics()
|
||||
{
|
||||
Vector2 mousePos = Mouse.current.position.ReadValue();
|
||||
Ray ray = mainCamera.ScreenPointToRay(mousePos);
|
||||
|
||||
// Where we want the object to be
|
||||
Vector3 targetPosition = ray.GetPoint(holdDistance);
|
||||
|
||||
// Calculate direction vector: Target - Current
|
||||
Vector3 direction = targetPosition - currentPickedRB.position;
|
||||
|
||||
// Apply Velocity
|
||||
// We multiply by pickupSpeed to make it responsive
|
||||
// The physics engine handles the rest (Mass 1 vs Mass 100 collisions)
|
||||
currentPickedRB.linearVelocity = direction * pickupSpeed;
|
||||
|
||||
// Note: In Unity versions older than 2023.3, use 'velocity' instead of 'linearVelocity'
|
||||
// currentPickedRB.velocity = direction * pickupSpeed;
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/ItemPickup.cs.meta
Normal file
2
Assets/Scripts/ItemPickup.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f02a9e864596f344699a392cf54fc364
|
||||
159
Assets/Scripts/PlayerController.cs
Normal file
159
Assets/Scripts/PlayerController.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
public class FirstPersonController : MonoBehaviour
|
||||
{
|
||||
[Header("Movement Settings")]
|
||||
public float speed = 5.0f;
|
||||
public float mouseSensitivity = 10.0f;
|
||||
|
||||
[Header("Camera Settings")]
|
||||
public Transform cameraTarget;
|
||||
public float minVerticalAngle = -90.0f;
|
||||
public float maxVerticalAngle = 90.0f;
|
||||
|
||||
private CharacterController controller;
|
||||
private Vector2 moveInput;
|
||||
private Vector2 lookInput;
|
||||
private float verticalRotation = 0f;
|
||||
private Vector3 velocity = Vector3.zero;
|
||||
private float gravity = 9.81f;
|
||||
|
||||
[Header("Input System")]
|
||||
public InputActionAsset inputActionAsset;
|
||||
private InputAction moveAction;
|
||||
private InputAction lookAction;
|
||||
private InputAction toggleMenuAction; // Single action for TAB
|
||||
|
||||
[Header("Shop Menu")]
|
||||
[SerializeField] GameObject inventoryMenuUI; // Current inventory
|
||||
[SerializeField] GameObject buyMenuUI; // Buy inventory
|
||||
|
||||
private bool isPaused = false; // Master state for Cursor + Camera
|
||||
|
||||
void Start()
|
||||
{
|
||||
controller = GetComponent<CharacterController>();
|
||||
|
||||
if (cameraTarget == null) Debug.LogError("Assign 'Camera Target'!");
|
||||
|
||||
// Initialize state: Locked cursor, Camera active
|
||||
SetPauseState(false);
|
||||
inventoryMenuUI.SetActive(false);
|
||||
buyMenuUI.SetActive(false);
|
||||
// Initialize shop menu UI
|
||||
}
|
||||
|
||||
void OnEnable()
|
||||
{
|
||||
if (inputActionAsset != null)
|
||||
{
|
||||
// Find Actions
|
||||
moveAction = inputActionAsset.FindAction("Move");
|
||||
lookAction = inputActionAsset.FindAction("Look");
|
||||
|
||||
// Make sure this name matches your Input Action Asset exactly!
|
||||
toggleMenuAction = inputActionAsset.FindAction("ToggleMenu");
|
||||
|
||||
// Enable Actions
|
||||
moveAction?.Enable();
|
||||
lookAction?.Enable();
|
||||
toggleMenuAction?.Enable();
|
||||
|
||||
// Subscribe to Events
|
||||
if (moveAction != null)
|
||||
{
|
||||
moveAction.performed += ctx => moveInput = ctx.ReadValue<Vector2>();
|
||||
moveAction.canceled += ctx => moveInput = Vector2.zero;
|
||||
}
|
||||
|
||||
if (lookAction != null)
|
||||
{
|
||||
lookAction.performed += ctx => lookInput = ctx.ReadValue<Vector2>();
|
||||
lookAction.canceled += ctx => lookInput = Vector2.zero;
|
||||
}
|
||||
|
||||
if (toggleMenuAction != null)
|
||||
{
|
||||
toggleMenuAction.performed += OnToggleMenuPerformed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnDisable()
|
||||
{
|
||||
moveAction?.Disable();
|
||||
lookAction?.Disable();
|
||||
toggleMenuAction?.Disable();
|
||||
}
|
||||
|
||||
// Triggered when TAB is pressed
|
||||
void OnToggleMenuPerformed(InputAction.CallbackContext context)
|
||||
{
|
||||
// Toggle the boolean state
|
||||
isPaused = !isPaused;
|
||||
|
||||
// Apply the new state
|
||||
SetPauseState(isPaused);
|
||||
}
|
||||
|
||||
// Central function to handle what happens when paused/unpaused
|
||||
void SetPauseState(bool paused)
|
||||
{
|
||||
if (paused)
|
||||
{
|
||||
// FREEZE: Show cursor, unlock mouse, stop camera
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
Cursor.visible = true;
|
||||
inventoryMenuUI.SetActive(true);
|
||||
buyMenuUI.SetActive(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
// RESUME: Hide cursor, lock mouse, allow camera
|
||||
Cursor.lockState = CursorLockMode.Locked;
|
||||
Cursor.visible = false;
|
||||
inventoryMenuUI.SetActive(false);
|
||||
buyMenuUI.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
// Always allow movement? Or restrict movement when paused?
|
||||
// If you want to stop walking when TAB is pressed, wrap this in "if (!isPaused)"
|
||||
HandleMovement();
|
||||
|
||||
// Only rotate camera if NOT paused
|
||||
if (!isPaused)
|
||||
{
|
||||
HandleRotation();
|
||||
}
|
||||
}
|
||||
|
||||
void HandleMovement()
|
||||
{
|
||||
Vector3 move = transform.right * moveInput.x + transform.forward * moveInput.y;
|
||||
|
||||
velocity.y -= gravity * Time.deltaTime;
|
||||
move.y = velocity.y;
|
||||
|
||||
controller.Move(move * speed * Time.deltaTime);
|
||||
}
|
||||
|
||||
void HandleRotation()
|
||||
{
|
||||
// Horizontal (Body)
|
||||
float mouseX = lookInput.x * mouseSensitivity * Time.deltaTime;
|
||||
transform.Rotate(Vector3.up * mouseX);
|
||||
|
||||
// Vertical (Camera Target)
|
||||
if (cameraTarget != null)
|
||||
{
|
||||
float mouseY = lookInput.y * mouseSensitivity * Time.deltaTime;
|
||||
verticalRotation -= mouseY;
|
||||
verticalRotation = Mathf.Clamp(verticalRotation, minVerticalAngle, maxVerticalAngle);
|
||||
cameraTarget.localRotation = Quaternion.Euler(verticalRotation, 0f, 0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
Assets/Scripts/PlayerController.cs.meta
Normal file
12
Assets/Scripts/PlayerController.cs.meta
Normal file
@@ -0,0 +1,12 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 120fca8f71f02a54d8e053397b5a0c4d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences:
|
||||
- inputActionAsset: {fileID: -944628639613478452, guid: 052faaac586de48259a63d0c4782560b, type: 3}
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
152
Assets/Scripts/RayCastSystem.cs
Normal file
152
Assets/Scripts/RayCastSystem.cs
Normal file
@@ -0,0 +1,152 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
|
||||
public class RayCastSystem : MonoBehaviour
|
||||
{
|
||||
[Header("Layer Settings")]
|
||||
public LayerMask interactableLayers;
|
||||
|
||||
[Header("Highlight Settings")]
|
||||
public Color highlightColor = new Color(1f, 1f, 0f, 0.35f);
|
||||
public float padding = 1.02f;
|
||||
|
||||
private Camera mainCamera;
|
||||
private GameObject currentSelection;
|
||||
private GameObject outlineObject;
|
||||
private MeshFilter outlineFilter;
|
||||
private MeshRenderer outlineRenderer;
|
||||
private Rigidbody currentPickedRB = null;
|
||||
|
||||
[Header("UI Elements")]
|
||||
public TMP_Text itemCountText;
|
||||
|
||||
void Start()
|
||||
{
|
||||
mainCamera = Camera.main;
|
||||
CreateOutlineObject();
|
||||
if(itemCountText) itemCountText.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
void CreateOutlineObject()
|
||||
{
|
||||
outlineObject = new GameObject("SelectionHighlight");
|
||||
outlineFilter = outlineObject.AddComponent<MeshFilter>();
|
||||
outlineRenderer = outlineObject.AddComponent<MeshRenderer>();
|
||||
|
||||
Material ghostMat = new Material(Shader.Find("Standard"));
|
||||
|
||||
// Optimize Material: Enable GPU Instancing
|
||||
ghostMat.enableInstancing = true;
|
||||
ghostMat.SetFloat("_Mode", 3); // Transparent
|
||||
ghostMat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha);
|
||||
ghostMat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha);
|
||||
ghostMat.SetInt("_ZWrite", 0);
|
||||
ghostMat.DisableKeyword("_ALPHATEST_ON");
|
||||
ghostMat.EnableKeyword("_ALPHABLEND_ON");
|
||||
ghostMat.DisableKeyword("_ALPHAPREMULTIPLY_ON");
|
||||
ghostMat.renderQueue = 3000;
|
||||
|
||||
ghostMat.color = highlightColor;
|
||||
ghostMat.EnableKeyword("_EMISSION");
|
||||
ghostMat.SetColor("_EmissionColor", highlightColor * 0.5f);
|
||||
|
||||
outlineRenderer.material = ghostMat;
|
||||
outlineObject.SetActive(false);
|
||||
|
||||
// OPTIMIZATION: Do not parent to the player.
|
||||
// Keeping it in the root ensures Scale calculations are always 1:1 with the target.
|
||||
outlineObject.transform.SetParent(null);
|
||||
}
|
||||
|
||||
void Update()
|
||||
{
|
||||
RaycastHit hit;
|
||||
// Perform Raycast
|
||||
if (Physics.Raycast(mainCamera.transform.position, mainCamera.transform.forward, out hit, 100f, interactableLayers))
|
||||
{
|
||||
GameObject hitObject = hit.collider.gameObject;
|
||||
|
||||
// Only run expensive logic if the selection ACTUALLY changed
|
||||
if (currentSelection != hitObject)
|
||||
{
|
||||
currentSelection = hitObject;
|
||||
UpdateOutline(currentSelection);
|
||||
UpdateUI(currentSelection); // Separated UI logic for cleanliness
|
||||
}
|
||||
|
||||
// Keep outline snapped
|
||||
if (currentSelection != null && outlineObject.activeSelf)
|
||||
{
|
||||
FollowTarget(currentSelection);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (currentSelection != null) ClearSelection();
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateUI(GameObject target)
|
||||
{
|
||||
if (itemCountText == null) return;
|
||||
|
||||
Shelf shelf = target.GetComponent<Shelf>();
|
||||
|
||||
if (shelf != null)
|
||||
{
|
||||
// PERFORMANCE FIX:
|
||||
// Instead of getting a whole dictionary, we just ask for the count directly.
|
||||
// You need to add 'public int ItemCount' or similar to your Shelf script.
|
||||
itemCountText.text = $"Items: {shelf.GetTotalItemCount()}";
|
||||
itemCountText.gameObject.SetActive(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
itemCountText.gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateOutline(GameObject target)
|
||||
{
|
||||
if(currentPickedRB != null) {
|
||||
outlineObject.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
MeshFilter targetMeshFilter = target.GetComponent<MeshFilter>();
|
||||
if (targetMeshFilter == null) targetMeshFilter = target.GetComponentInChildren<MeshFilter>();
|
||||
|
||||
if (targetMeshFilter == null)
|
||||
{
|
||||
outlineObject.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Lightweight: Just swapping the reference, not creating new meshes
|
||||
outlineFilter.sharedMesh = targetMeshFilter.sharedMesh;
|
||||
|
||||
FollowTarget(target);
|
||||
outlineObject.SetActive(true);
|
||||
}
|
||||
|
||||
void FollowTarget(GameObject target)
|
||||
{
|
||||
outlineObject.transform.position = target.transform.position;
|
||||
outlineObject.transform.rotation = target.transform.rotation;
|
||||
// This math is safer now that outlineObject is not a child of the Player
|
||||
outlineObject.transform.localScale = target.transform.localScale * padding;
|
||||
}
|
||||
|
||||
void ClearSelection()
|
||||
{
|
||||
currentSelection = null;
|
||||
outlineObject.SetActive(false);
|
||||
if (itemCountText) itemCountText.gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
public void SetCurrentPickedRB(Rigidbody rb)
|
||||
{
|
||||
currentPickedRB = rb;
|
||||
if (currentPickedRB != null) ClearSelection();
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/RayCastSystem.cs.meta
Normal file
2
Assets/Scripts/RayCastSystem.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ee95aee08f2e5254d86b6044d4ba64f6
|
||||
215
Assets/Scripts/Shelf.cs
Normal file
215
Assets/Scripts/Shelf.cs
Normal file
@@ -0,0 +1,215 @@
|
||||
using UnityEngine;
|
||||
using System.Collections.Generic;
|
||||
|
||||
[RequireComponent(typeof(BoxCollider))]
|
||||
public class Shelf : MonoBehaviour
|
||||
{
|
||||
[Header("Grid Settings")]
|
||||
[Tooltip("How many items fit horizontally")]
|
||||
public int columns = 5;
|
||||
[Tooltip("How many items fit vertically/deep")]
|
||||
public int rows = 2;
|
||||
|
||||
[Header("Snap Settings")]
|
||||
// Lift item slightly so it sits ON the shelf, not IN it
|
||||
public Vector3 offset = new Vector3(0, 0.1f, 0);
|
||||
|
||||
// Track which object is in which slot. Index 0 = Slot 0.
|
||||
private GameObject[] slotOccupants;
|
||||
private Vector3[] localGridPositions;
|
||||
private BoxCollider shelfCollider;
|
||||
|
||||
void Start()
|
||||
{
|
||||
shelfCollider = GetComponent<BoxCollider>();
|
||||
// Ensure the collider is a trigger so items can pass through to be detected
|
||||
shelfCollider.isTrigger = true;
|
||||
|
||||
CalculateGridPositions();
|
||||
|
||||
// Initialize occupancy array
|
||||
slotOccupants = new GameObject[columns * rows];
|
||||
}
|
||||
|
||||
[ContextMenu("Recalculate Grid")]
|
||||
void CalculateGridPositions()
|
||||
{
|
||||
if(shelfCollider == null) shelfCollider = GetComponent<BoxCollider>();
|
||||
|
||||
int totalSlots = columns * rows;
|
||||
localGridPositions = new Vector3[totalSlots];
|
||||
|
||||
// Get dimensions of the collider
|
||||
float width = shelfCollider.size.x;
|
||||
float depth = shelfCollider.size.z; // Assuming Y is Up, Z is depth
|
||||
|
||||
// Calculate cell size
|
||||
float cellWidth = width / columns;
|
||||
float cellDepth = depth / rows;
|
||||
|
||||
// Calculate starting point (Bottom-Left of the shelf surface)
|
||||
Vector3 startPos = shelfCollider.center - new Vector3(width / 2, -shelfCollider.size.y/2, depth / 2);
|
||||
startPos += new Vector3(cellWidth / 2, 0, cellDepth / 2);
|
||||
|
||||
int index = 0;
|
||||
for (int r = 0; r < rows; r++)
|
||||
{
|
||||
for (int c = 0; c < columns; c++)
|
||||
{
|
||||
// Calculate local position
|
||||
Vector3 pos = startPos + new Vector3(c * cellWidth, 0, r * cellDepth);
|
||||
localGridPositions[index] = pos;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void OnTriggerEnter(Collider other)
|
||||
{
|
||||
// 1. Check if it is an Item
|
||||
// 2. Check if we aren't ALREADY tracking it (prevents glitching)
|
||||
if (other.GetComponent<Item>() != null && !IsItemOnShelf(other.gameObject))
|
||||
{
|
||||
TrySnapItem(other.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
void OnTriggerExit(Collider other)
|
||||
{
|
||||
if (other.GetComponent<Item>() != null)
|
||||
{
|
||||
RemoveItem(other.gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
void TrySnapItem(GameObject itemObj)
|
||||
{
|
||||
int bestSlotIndex = -1;
|
||||
float closestDistance = float.MaxValue;
|
||||
|
||||
// --- NEW LOGIC: FIND CLOSEST EMPTY SLOT ---
|
||||
// Instead of grabbing the first empty one (which causes teleporting),
|
||||
// we check distance to find the slot closest to where you are holding the item.
|
||||
for (int i = 0; i < localGridPositions.Length; i++)
|
||||
{
|
||||
// Only check EMPTY slots
|
||||
if (slotOccupants[i] == null)
|
||||
{
|
||||
// Convert local grid point to world space to compare distance
|
||||
Vector3 worldSlotPos = transform.TransformPoint(localGridPositions[i]);
|
||||
float dist = Vector3.Distance(itemObj.transform.position, worldSlotPos);
|
||||
|
||||
// If this slot is closer than the last one we found, pick it
|
||||
if (dist < closestDistance)
|
||||
{
|
||||
closestDistance = dist;
|
||||
bestSlotIndex = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If shelf is full, do nothing
|
||||
if (bestSlotIndex == -1) return;
|
||||
|
||||
// --- SNAP LOGIC ---
|
||||
slotOccupants[bestSlotIndex] = itemObj;
|
||||
|
||||
// --- PHYSICS FIX ---
|
||||
Rigidbody rb = itemObj.GetComponent<Rigidbody>();
|
||||
if(rb != null)
|
||||
{
|
||||
// 1. Stop the object FIRST
|
||||
rb.linearVelocity = Vector3.zero;
|
||||
rb.angularVelocity = Vector3.zero;
|
||||
|
||||
// 2. THEN disable physics interactions
|
||||
rb.isKinematic = true;
|
||||
}
|
||||
|
||||
// Move item to the best slot found
|
||||
Vector3 targetPos = transform.TransformPoint(localGridPositions[bestSlotIndex]) + offset;
|
||||
itemObj.transform.position = targetPos;
|
||||
itemObj.transform.rotation = transform.rotation;
|
||||
}
|
||||
|
||||
void RemoveItem(GameObject itemObj)
|
||||
{
|
||||
// Find where this item was and clear the slot
|
||||
for (int i = 0; i < slotOccupants.Length; i++)
|
||||
{
|
||||
if (slotOccupants[i] == itemObj)
|
||||
{
|
||||
slotOccupants[i] = null;
|
||||
|
||||
// Re-enable physics when leaving
|
||||
Rigidbody rb = itemObj.GetComponent<Rigidbody>();
|
||||
if (rb != null) rb.isKinematic = false;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to check if item is already physically tracked
|
||||
bool IsItemOnShelf(GameObject item)
|
||||
{
|
||||
for (int i = 0; i < slotOccupants.Length; i++)
|
||||
{
|
||||
if (slotOccupants[i] == item) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- INVENTORY LOGIC MERGED ---
|
||||
// This replaces your old Dictionary. It counts the actual physical items.
|
||||
// This is safer because it can never get "out of sync" with what you see.
|
||||
public int GetTotalItemCount()
|
||||
{
|
||||
int count = 0;
|
||||
if (slotOccupants != null)
|
||||
{
|
||||
foreach (var item in slotOccupants)
|
||||
{
|
||||
if (item != null) count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// DRAW THE GRID IN EDITOR SO WE CAN SEE IT
|
||||
void OnDrawGizmos()
|
||||
{
|
||||
if (shelfCollider == null) shelfCollider = GetComponent<BoxCollider>();
|
||||
|
||||
// Visualize the cached positions if game is running
|
||||
if (Application.isPlaying && localGridPositions != null)
|
||||
{
|
||||
Gizmos.color = Color.green;
|
||||
foreach (Vector3 localPos in localGridPositions)
|
||||
{
|
||||
Vector3 worldPos = transform.TransformPoint(localPos);
|
||||
Gizmos.DrawWireSphere(worldPos, 0.05f);
|
||||
Gizmos.DrawWireCube(worldPos + offset, new Vector3(0.1f, 0.1f, 0.1f));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback for Editor Mode (Logic repeated for visual aid before playing)
|
||||
Gizmos.color = Color.yellow;
|
||||
// ... Simple calc for editor preview only ...
|
||||
float w = shelfCollider.size.x;
|
||||
float d = shelfCollider.size.z;
|
||||
float cw = w / columns;
|
||||
float cd = d / rows;
|
||||
Vector3 start = shelfCollider.center - new Vector3(w/2, -shelfCollider.size.y/2, d/2) + new Vector3(cw/2, 0, cd/2);
|
||||
|
||||
for(int r=0; r<rows; r++) {
|
||||
for(int c=0; c<columns; c++) {
|
||||
Vector3 pos = start + new Vector3(c*cw, 0, r*cd);
|
||||
Vector3 worldPos = transform.TransformPoint(pos);
|
||||
Gizmos.DrawWireSphere(worldPos, 0.05f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Shelf.cs.meta
Normal file
2
Assets/Scripts/Shelf.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2979aa091d840f243b5ebda40c3ee41a
|
||||
159
Assets/Scripts/ShopManager.cs
Normal file
159
Assets/Scripts/ShopManager.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using System.Collections.Generic;
|
||||
using TMPro;
|
||||
|
||||
public class ShopManager : MonoBehaviour
|
||||
{
|
||||
[Header("Shop Settings")]
|
||||
public int startingMoney = 100;
|
||||
public Item[] availableItems; // Assumes Item is a MonoBehaviour attached to a Prefab
|
||||
|
||||
[Header("UI Elements")]
|
||||
public TMP_Text moneyText;
|
||||
public Button[] buyButtons;
|
||||
|
||||
[Header("Inventory Settings")]
|
||||
public Transform storeRoomTransform; // Where physical items spawn
|
||||
public Transform inventoryUI; // The Panel with the Grid Layout Group
|
||||
public GameObject inventoryTextTemplate; // Drag a Prefab with TMP_Text here!
|
||||
|
||||
private int currentMoney;
|
||||
private Dictionary<string, int> inventory = new Dictionary<string, int>();
|
||||
|
||||
void Start()
|
||||
{
|
||||
currentMoney = startingMoney;
|
||||
|
||||
// Initialize dictionary once at start
|
||||
PopulateInventoryDictionary();
|
||||
|
||||
InitializeShopButtons();
|
||||
UpdateAllUI();
|
||||
}
|
||||
|
||||
void InitializeShopButtons()
|
||||
{
|
||||
for (int i = 0; i < buyButtons.Length; i++)
|
||||
{
|
||||
if (i < availableItems.Length)
|
||||
{
|
||||
int index = i;
|
||||
Item item = availableItems[index];
|
||||
Button button = buyButtons[index];
|
||||
|
||||
// Set Button Text
|
||||
TMP_Text btnText = button.GetComponentInChildren<TMP_Text>();
|
||||
if (btnText != null)
|
||||
{
|
||||
btnText.text = $"{item.itemName} - ${item.itemPrice}";
|
||||
}
|
||||
|
||||
// Add Click Listener
|
||||
button.onClick.RemoveAllListeners();
|
||||
button.onClick.AddListener(() => BuyItem(item, button));
|
||||
}
|
||||
else
|
||||
{
|
||||
buyButtons[i].gameObject.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BuyItem(Item item, Button button)
|
||||
{
|
||||
if (currentMoney >= item.itemPrice)
|
||||
{
|
||||
// 1. Deduct Money
|
||||
currentMoney -= item.itemPrice;
|
||||
|
||||
// 2. Instantiate physical item in the game world
|
||||
if(item.gameObject != null)
|
||||
{
|
||||
Instantiate(item.gameObject, storeRoomTransform.position, Quaternion.identity);
|
||||
}
|
||||
|
||||
// 3. Logic: Add to dictionary
|
||||
if (!inventory.ContainsKey(item.itemName))
|
||||
{
|
||||
inventory[item.itemName] = 0;
|
||||
}
|
||||
inventory[item.itemName]++;
|
||||
|
||||
Debug.Log($"Purchased {item.itemName}");
|
||||
|
||||
// 4. Update Button Feedback (Optional)
|
||||
TMP_Text btnText = button.GetComponentInChildren<TMP_Text>();
|
||||
if (btnText != null)
|
||||
{
|
||||
// Temporarily show "Bought" or keep price?
|
||||
// Usually shops keep the price so you can buy again.
|
||||
// If you want it to flash green, you'd need a Coroutine,
|
||||
// but for now let's keep it simple.
|
||||
}
|
||||
|
||||
// 5. Refresh UI
|
||||
UpdateAllUI();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log("Not enough money!");
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateAllUI()
|
||||
{
|
||||
// Update Money Text
|
||||
moneyText.text = "Money: $" + currentMoney;
|
||||
|
||||
// Update Button Interactability
|
||||
for (int i = 0; i < buyButtons.Length; i++)
|
||||
{
|
||||
if (i < availableItems.Length)
|
||||
{
|
||||
buyButtons[i].interactable = (currentMoney >= availableItems[i].itemPrice);
|
||||
}
|
||||
}
|
||||
|
||||
// Update Inventory List
|
||||
UpdateInventoryUI();
|
||||
}
|
||||
|
||||
void PopulateInventoryDictionary()
|
||||
{
|
||||
foreach (Item item in availableItems)
|
||||
{
|
||||
if (!inventory.ContainsKey(item.itemName))
|
||||
{
|
||||
inventory[item.itemName] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateInventoryUI()
|
||||
{
|
||||
// 1. Clear existing list items
|
||||
foreach (Transform child in inventoryUI)
|
||||
{
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
|
||||
// 2. Create new list items from the Template
|
||||
foreach (KeyValuePair<string, int> entry in inventory)
|
||||
{
|
||||
// Only show items we actually have (Count > 0)
|
||||
if(entry.Value > 0)
|
||||
{
|
||||
// Instantiate the Template (which has the TMP_Text component)
|
||||
GameObject newItem = Instantiate(inventoryTextTemplate, inventoryUI);
|
||||
|
||||
// Get the text component and set the string
|
||||
TMP_Text textComp = newItem.GetComponent<TMP_Text>();
|
||||
if (textComp != null)
|
||||
{
|
||||
textComp.text = $"{entry.Key}: {entry.Value}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/ShopManager.cs.meta
Normal file
2
Assets/Scripts/ShopManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f0b525ab928b6984080f970083022691
|
||||
Reference in New Issue
Block a user