working on quest and interaction system
This commit is contained in:
24
Assets/Scripts/Billboard.cs
Normal file
24
Assets/Scripts/Billboard.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Place this on your WorldSpace Canvas to make it always face the player's camera!
|
||||
/// </summary>
|
||||
public class Billboard : MonoBehaviour
|
||||
{
|
||||
private Camera _mainCamera;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_mainCamera = Camera.main;
|
||||
}
|
||||
|
||||
private void LateUpdate()
|
||||
{
|
||||
if (_mainCamera != null)
|
||||
{
|
||||
// Makes the Canvas look exactly at the camera, then flips it 180 degrees
|
||||
// so the text isn't backwards!
|
||||
transform.LookAt(transform.position + _mainCamera.transform.forward);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/Billboard.cs.meta
Normal file
2
Assets/Scripts/Billboard.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 659304d8c0f20275db2874749c7f48b9
|
||||
@@ -93,6 +93,7 @@ public class BowAimLayerController : MonoBehaviour
|
||||
|
||||
// Are we currently holding the draw button (left click)?
|
||||
private bool _isDrawing;
|
||||
public bool IsDrawing => _isDrawing;
|
||||
private Projectile _loadedArrow;
|
||||
|
||||
private float _drawStartTime;
|
||||
|
||||
27
Assets/Scripts/IInteractable.cs
Normal file
27
Assets/Scripts/IInteractable.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Any object in the game that the player can interact with MUST implement this interface!
|
||||
/// </summary>
|
||||
public interface IInteractable
|
||||
{
|
||||
/// <summary>
|
||||
/// The primary text that shows up in the UI (e.g. "Collect", "Open")
|
||||
/// </summary>
|
||||
string GetInteractText();
|
||||
|
||||
/// <summary>
|
||||
/// The secondary text that shows up in the UI (e.g. "Apple", "Wooden Door")
|
||||
/// </summary>
|
||||
string GetSecondaryInteractText();
|
||||
|
||||
/// <summary>
|
||||
/// Can the player currently interact with this? (E.g. false if a chest is already open)
|
||||
/// </summary>
|
||||
bool CanInteract();
|
||||
|
||||
/// <summary>
|
||||
/// What happens when the player presses E!
|
||||
/// </summary>
|
||||
void Interact(GameObject player);
|
||||
}
|
||||
2
Assets/Scripts/IInteractable.cs.meta
Normal file
2
Assets/Scripts/IInteractable.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4dae0bc97016390eea52457972414611
|
||||
100
Assets/Scripts/InteractionController.cs
Normal file
100
Assets/Scripts/InteractionController.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
public class InteractionController : MonoBehaviour
|
||||
{
|
||||
[Header("Interaction Settings")]
|
||||
[Tooltip("How large the proximity bubble is around the player (e.g. 3 meters).")]
|
||||
public float interactionDistance = 3f;
|
||||
[Tooltip("Layers that contain interactable objects (e.g. NPC, Default)")]
|
||||
public LayerMask interactableLayers;
|
||||
|
||||
[Header("References")]
|
||||
public PlayerUIManager uiManager;
|
||||
|
||||
[Header("Input")]
|
||||
[Tooltip("The key used to interact")]
|
||||
public Key interactKey = Key.E;
|
||||
|
||||
// We store whatever we are currently closest to
|
||||
private IInteractable _currentInteractable;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (uiManager == null) return;
|
||||
|
||||
// Create a sphere around the player to find all interactables in range
|
||||
// Use QueryTriggerInteraction.Ignore so we don't accidentally interact with giant invisible AI/Aggro triggers!
|
||||
Collider[] hits = Physics.OverlapSphere(transform.position, interactionDistance, interactableLayers, QueryTriggerInteraction.Ignore);
|
||||
|
||||
IInteractable closestInteractable = null;
|
||||
float closestDistance = float.MaxValue;
|
||||
|
||||
// Loop through everything in our bubble to find the closest one
|
||||
foreach (var hit in hits)
|
||||
{
|
||||
// Ignore our own body
|
||||
if (hit.transform.root == transform.root) continue;
|
||||
|
||||
IInteractable interactable = hit.GetComponentInParent<IInteractable>();
|
||||
if (interactable != null && interactable.CanInteract())
|
||||
{
|
||||
float dist = Vector3.Distance(transform.position, hit.ClosestPoint(transform.position));
|
||||
|
||||
if (dist < closestDistance)
|
||||
{
|
||||
closestDistance = dist;
|
||||
closestInteractable = interactable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we found something valid nearby
|
||||
if (closestInteractable != null)
|
||||
{
|
||||
if (_currentInteractable != closestInteractable)
|
||||
{
|
||||
_currentInteractable = closestInteractable;
|
||||
ShowPrompt(closestInteractable.GetInteractText(), closestInteractable.GetSecondaryInteractText());
|
||||
}
|
||||
|
||||
// Listen for input
|
||||
if (Keyboard.current != null && Keyboard.current[interactKey].wasPressedThisFrame)
|
||||
{
|
||||
closestInteractable.Interact(gameObject);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Nothing in range, or closest thing can't be interacted with anymore
|
||||
if (_currentInteractable != null)
|
||||
{
|
||||
_currentInteractable = null;
|
||||
HidePrompt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ShowPrompt(string actionText, string secondaryText)
|
||||
{
|
||||
if (uiManager.interactionPanel != null) uiManager.interactionPanel.SetActive(true);
|
||||
if (uiManager.interactionText != null) uiManager.interactionText.text = actionText;
|
||||
|
||||
if (uiManager.interactionSecondaryText != null)
|
||||
{
|
||||
uiManager.interactionSecondaryText.text = secondaryText;
|
||||
}
|
||||
}
|
||||
|
||||
private void HidePrompt()
|
||||
{
|
||||
if (uiManager.interactionPanel != null) uiManager.interactionPanel.SetActive(false);
|
||||
}
|
||||
|
||||
// Draws a helpful visual bubble in the Scene view so you can see your interaction range!
|
||||
private void OnDrawGizmosSelected()
|
||||
{
|
||||
Gizmos.color = new Color(0f, 1f, 0f, 0.3f);
|
||||
Gizmos.DrawSphere(transform.position, interactionDistance);
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/InteractionController.cs.meta
Normal file
2
Assets/Scripts/InteractionController.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 59ab99a089979717c8f5237412eb6c67
|
||||
@@ -351,6 +351,13 @@ public class NPCController : MonoBehaviour
|
||||
private void Die()
|
||||
{
|
||||
_isDead = true;
|
||||
|
||||
// Check if this NPC is a quest target!
|
||||
QuestInteractable questTarget = GetComponent<QuestInteractable>();
|
||||
if (questTarget != null)
|
||||
{
|
||||
questTarget.TriggerQuestProgress();
|
||||
}
|
||||
|
||||
if (_agent != null && _agent.isOnNavMesh)
|
||||
{
|
||||
|
||||
@@ -24,6 +24,28 @@ public class PlayerUIManager : MonoBehaviour
|
||||
[Tooltip("A Slider that fills up from 0 to 1 as the bow is drawn.")]
|
||||
public Slider bowDrawBar;
|
||||
|
||||
[Header("Quest UI")]
|
||||
[Tooltip("The parent GameObject that holds all the Quest UI elements (allows us to hide the whole window when empty)")]
|
||||
public GameObject questUIPanel;
|
||||
[Tooltip("The TextMeshPro text for the name of the Quest")]
|
||||
public TMPro.TextMeshProUGUI questNameText;
|
||||
[Tooltip("The container transform where the Objective prefabs will be spawned (e.g., a Vertical Layout Group)")]
|
||||
public Transform objectivesContainer;
|
||||
[Tooltip("The prefab for a single objective row (Should have the QuestObjectiveUI script on it)")]
|
||||
public QuestObjectiveUI objectivePrefab;
|
||||
|
||||
[Header("Interaction UI")]
|
||||
[Tooltip("The parent GameObject for the interaction popup")]
|
||||
public GameObject interactionPanel;
|
||||
[Tooltip("The TMPro Text that shows the primary interaction prompt (e.g., 'Collect')")]
|
||||
public TMPro.TextMeshProUGUI interactionText;
|
||||
[Tooltip("The secondary TMPro Text for the interaction prompt (e.g., 'Press E' or the Item Name)")]
|
||||
public TMPro.TextMeshProUGUI interactionSecondaryText;
|
||||
|
||||
[Header("Crosshair UI")]
|
||||
[Tooltip("The crosshair image in the center of the screen")]
|
||||
public GameObject crosshairImage;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (playerStats == null)
|
||||
@@ -42,12 +64,91 @@ public class PlayerUIManager : MonoBehaviour
|
||||
bowDrawBar.maxValue = 1f;
|
||||
bowDrawBar.value = 0f;
|
||||
}
|
||||
|
||||
// Try to connect to the Quest Manager to listen for updates
|
||||
if (QuestManager.Instance != null)
|
||||
{
|
||||
QuestManager.Instance.onQuestUpdated += DrawQuestUI;
|
||||
DrawQuestUI(); // Draw initial state
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// Unsubscribe to prevent memory leaks
|
||||
if (QuestManager.Instance != null)
|
||||
{
|
||||
QuestManager.Instance.onQuestUpdated -= DrawQuestUI;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawQuestUI()
|
||||
{
|
||||
// If there's no Quest Manager or no active quest, hide the UI completely
|
||||
if (QuestManager.Instance == null || QuestManager.Instance.activeQuest == null || string.IsNullOrEmpty(QuestManager.Instance.activeQuest.questName))
|
||||
{
|
||||
if (questUIPanel != null) questUIPanel.SetActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
Quest activeQuest = QuestManager.Instance.activeQuest;
|
||||
|
||||
// 1. Show the panel and update the Name
|
||||
if (questUIPanel != null) questUIPanel.SetActive(true);
|
||||
if (questNameText != null) questNameText.text = activeQuest.questName;
|
||||
|
||||
// 2. Clear existing objectives in the container
|
||||
if (objectivesContainer != null)
|
||||
{
|
||||
foreach (Transform child in objectivesContainer)
|
||||
{
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
|
||||
// 3. Spawn a prefab for each objective
|
||||
if (objectivePrefab != null)
|
||||
{
|
||||
bool foundActive = false;
|
||||
foreach (var objective in activeQuest.objectives)
|
||||
{
|
||||
QuestObjectiveUI newObjUI = Instantiate(objectivePrefab, objectivesContainer);
|
||||
|
||||
// Determine if THIS objective is the active one
|
||||
// (The first incomplete objective in the list)
|
||||
bool isThisActive = false;
|
||||
if (!objective.isCompleted && !foundActive)
|
||||
{
|
||||
isThisActive = true;
|
||||
foundActive = true;
|
||||
}
|
||||
|
||||
string countText = "";
|
||||
if (objective.requiresCount)
|
||||
{
|
||||
countText = $" ({objective.currentCount}/{objective.targetCount})";
|
||||
}
|
||||
|
||||
newObjUI.Setup(objective.description, isThisActive, objective.isCompleted, countText);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
UpdateBars();
|
||||
UpdateCombatUI();
|
||||
UpdateCrosshair();
|
||||
}
|
||||
|
||||
private void UpdateCrosshair()
|
||||
{
|
||||
if (crosshairImage != null)
|
||||
{
|
||||
// Only show crosshair if we have a bow equipped and are currently holding aim/draw
|
||||
bool shouldShow = bowAimLayerController != null && bowAimLayerController.IsAiming && bowAimLayerController.IsDrawing;
|
||||
crosshairImage.SetActive(shouldShow);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateBars()
|
||||
|
||||
172
Assets/Scripts/QuestInteractable.cs
Normal file
172
Assets/Scripts/QuestInteractable.cs
Normal file
@@ -0,0 +1,172 @@
|
||||
using UnityEngine;
|
||||
|
||||
/// <summary>
|
||||
/// Place this on an NPC, Item, or Trigger Zone to easily connect it to the Quest System!
|
||||
/// </summary>
|
||||
public class QuestInteractable : MonoBehaviour, IInteractable
|
||||
{
|
||||
[Header("Quest Progression")]
|
||||
[Tooltip("What action does this object trigger?")]
|
||||
public ObjectiveType objectiveType;
|
||||
|
||||
[Tooltip("The unique ID that matches the QuestObjective (e.g., 'wolf', 'ancient_relic')")]
|
||||
public string targetID;
|
||||
|
||||
[Tooltip("How much to progress the objective (usually 1)")]
|
||||
public int progressAmount = 1;
|
||||
|
||||
[Header("Interaction Settings")]
|
||||
[Tooltip("Can the player walk up and press E on this object?")]
|
||||
public bool isInteractable = false;
|
||||
[Tooltip("If true, the player can ONLY interact with this if they currently have the matching quest objective active!")]
|
||||
public bool requireActiveObjective = false;
|
||||
[Tooltip("The action verb for the prompt (e.g., 'Open', 'Collect', 'Steal')")]
|
||||
public string interactAction = "Interact";
|
||||
[Tooltip("The secondary text to display (e.g. the name of the item or 'Press E')")]
|
||||
public string interactTargetName = "[ E ]";
|
||||
|
||||
[Header("Destination/Trigger Settings")]
|
||||
[Tooltip("Check this if walking into a Trigger Collider should automatically complete the objective.")]
|
||||
public bool completeOnTriggerEnter = false;
|
||||
[Tooltip("A visual marker GameObject to enable while active, and disable when completed.")]
|
||||
public GameObject destinationMarker;
|
||||
[Tooltip("A TMPro text element to show the distance counting down as the player gets closer.")]
|
||||
public TMPro.TextMeshProUGUI distanceText;
|
||||
|
||||
[Header("Dynamic Marker Height")]
|
||||
[Tooltip("If true, the marker will physically raise into the sky when far away, and lower to the ground when close.")]
|
||||
public bool scaleMarkerHeight = true;
|
||||
public float maxDistanceForHeight = 50f;
|
||||
public float minHeight = 1.5f;
|
||||
public float maxHeight = 15f;
|
||||
[Tooltip("How far from the center the marker should hit its lowest point. If you have a large Box Collider, set this to half its width (e.g., 3m) so the marker bottoms out right as you touch the edge!")]
|
||||
public float arrivalDistanceOffset = 3f;
|
||||
|
||||
[Header("Item Settings")]
|
||||
[Tooltip("Check this if this is an item that should destroy itself when collected/triggered")]
|
||||
public bool destroyOnTrigger;
|
||||
|
||||
private bool _hasBeenTriggered = false;
|
||||
private Transform _playerTransform;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Find player once at start for distance tracking
|
||||
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
|
||||
if (playerObj != null)
|
||||
{
|
||||
_playerTransform = playerObj.transform;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Update distance countdown if marker is active and we haven't triggered
|
||||
if (completeOnTriggerEnter && !_hasBeenTriggered && destinationMarker != null && destinationMarker.activeInHierarchy)
|
||||
{
|
||||
// Fallback: If the 'Player' tag is on a stationary object or missing, track the Main Camera!
|
||||
// The Main Camera is guaranteed to move with you, so the distance will always update correctly.
|
||||
if (_playerTransform == null || _playerTransform.position == Vector3.zero)
|
||||
{
|
||||
if (Camera.main != null) _playerTransform = Camera.main.transform;
|
||||
}
|
||||
|
||||
if (_playerTransform != null)
|
||||
{
|
||||
float dist = Vector3.Distance(_playerTransform.position, transform.position);
|
||||
|
||||
if (distanceText != null)
|
||||
{
|
||||
distanceText.text = $"{Mathf.RoundToInt(dist)}m";
|
||||
}
|
||||
|
||||
if (scaleMarkerHeight)
|
||||
{
|
||||
// Subtract the offset so it bottoms out at the edge of the collider instead of the exact center
|
||||
float effectiveDist = Mathf.Max(0, dist - arrivalDistanceOffset);
|
||||
|
||||
// Calculate a 0 to 1 value (0 = at the edge, 1 = max distance away)
|
||||
float t = Mathf.Clamp01(effectiveDist / maxDistanceForHeight);
|
||||
|
||||
// Lerp between our min and max height clamps
|
||||
float targetY = Mathf.Lerp(minHeight, maxHeight, t);
|
||||
|
||||
Vector3 localPos = destinationMarker.transform.localPosition;
|
||||
localPos.y = targetY;
|
||||
destinationMarker.transform.localPosition = localPos;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (!completeOnTriggerEnter || _hasBeenTriggered) return;
|
||||
|
||||
// Ensure it's the player that walked into the trigger
|
||||
if (other.CompareTag("Player") || other.transform.root.CompareTag("Player") || other.GetComponentInParent<PlayerStats>() != null)
|
||||
{
|
||||
TriggerQuestProgress();
|
||||
}
|
||||
}
|
||||
|
||||
// --- IInteractable Implementation ---
|
||||
public string GetInteractText()
|
||||
{
|
||||
return interactAction;
|
||||
}
|
||||
|
||||
public string GetSecondaryInteractText()
|
||||
{
|
||||
return interactTargetName;
|
||||
}
|
||||
|
||||
public bool CanInteract()
|
||||
{
|
||||
if (!isInteractable || _hasBeenTriggered) return false;
|
||||
|
||||
// If this option is checked, we ask the QuestManager if this is our current objective!
|
||||
if (requireActiveObjective)
|
||||
{
|
||||
if (QuestManager.Instance == null) return false;
|
||||
return QuestManager.Instance.IsObjectiveActive(objectiveType, targetID);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Interact(GameObject player)
|
||||
{
|
||||
TriggerQuestProgress();
|
||||
}
|
||||
// ------------------------------------
|
||||
|
||||
/// <summary>
|
||||
/// Call this from your existing interaction scripts, or a Unity Event when picking up an item!
|
||||
/// </summary>
|
||||
public void TriggerQuestProgress()
|
||||
{
|
||||
if (_hasBeenTriggered) return;
|
||||
|
||||
bool wasSuccessful = false;
|
||||
if (QuestManager.Instance != null)
|
||||
{
|
||||
wasSuccessful = QuestManager.Instance.ProgressObjective(objectiveType, targetID, progressAmount);
|
||||
}
|
||||
|
||||
// If the objective wasn't active (or was wrong type), we ignore it so they can try again later
|
||||
if (!wasSuccessful) return;
|
||||
|
||||
_hasBeenTriggered = true;
|
||||
|
||||
if (destinationMarker != null)
|
||||
{
|
||||
destinationMarker.SetActive(false);
|
||||
}
|
||||
|
||||
if (destroyOnTrigger)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/QuestInteractable.cs.meta
Normal file
2
Assets/Scripts/QuestInteractable.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 831349a993aa5fa29be4435af114fe21
|
||||
178
Assets/Scripts/QuestManager.cs
Normal file
178
Assets/Scripts/QuestManager.cs
Normal file
@@ -0,0 +1,178 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.InputSystem;
|
||||
|
||||
public enum ObjectiveType { Interact, Kill, Collect, None }
|
||||
|
||||
[System.Serializable]
|
||||
public class QuestObjective
|
||||
{
|
||||
[Tooltip("Unique ID for this objective (e.g., 'bandit_leader', 'wolf_pelt')")]
|
||||
public string targetID;
|
||||
|
||||
[Tooltip("What action the player must perform")]
|
||||
public ObjectiveType type;
|
||||
|
||||
[Tooltip("The text shown in the UI")]
|
||||
public string description;
|
||||
|
||||
[Header("Counting")]
|
||||
public bool requiresCount;
|
||||
public int currentCount;
|
||||
public int targetCount;
|
||||
|
||||
[HideInInspector]
|
||||
public bool isCompleted;
|
||||
}
|
||||
|
||||
[System.Serializable]
|
||||
public class Quest
|
||||
{
|
||||
public string questName;
|
||||
public List<QuestObjective> objectives = new List<QuestObjective>();
|
||||
public bool isCompleted;
|
||||
}
|
||||
|
||||
public class QuestManager : MonoBehaviour
|
||||
{
|
||||
public static QuestManager Instance { get; private set; }
|
||||
|
||||
[Header("Current Quest")]
|
||||
public Quest activeQuest;
|
||||
|
||||
[Header("Testing")]
|
||||
[Tooltip("Set the key to automatically progress the first incomplete objective")]
|
||||
public Key completeObjectiveKey = Key.O;
|
||||
|
||||
// We notify the UI when the quest updates
|
||||
public delegate void OnQuestUpdated();
|
||||
public event OnQuestUpdated onQuestUpdated;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance == null)
|
||||
{
|
||||
Instance = this;
|
||||
}
|
||||
else
|
||||
{
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// Tell UI to draw whatever quest is assigned in the inspector (if any)
|
||||
NotifyUI();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Keyboard.current != null && Keyboard.current[completeObjectiveKey].wasPressedThisFrame)
|
||||
{
|
||||
// For testing: progress the first incomplete objective
|
||||
if (activeQuest != null && !activeQuest.isCompleted)
|
||||
{
|
||||
foreach (var obj in activeQuest.objectives)
|
||||
{
|
||||
if (!obj.isCompleted)
|
||||
{
|
||||
ProgressObjective(obj.type, obj.targetID, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// NPCs or Items can call this to progress the current quest!
|
||||
/// Example: QuestManager.Instance.ProgressObjective(ObjectiveType.Kill, "wolf");
|
||||
/// </summary>
|
||||
public bool ProgressObjective(ObjectiveType type, string targetID, int amount = 1)
|
||||
{
|
||||
if (activeQuest == null || activeQuest.isCompleted) return false;
|
||||
|
||||
foreach (var obj in activeQuest.objectives)
|
||||
{
|
||||
// Only progress the FIRST incomplete objective (Linear Quests)
|
||||
if (!obj.isCompleted)
|
||||
{
|
||||
// Check if this objective matches the action
|
||||
if (obj.type == type && obj.targetID == targetID)
|
||||
{
|
||||
if (obj.requiresCount)
|
||||
{
|
||||
obj.currentCount += amount;
|
||||
Debug.Log($"[QuestManager] Progressed '{obj.description}': {obj.currentCount}/{obj.targetCount}");
|
||||
|
||||
if (obj.currentCount >= obj.targetCount)
|
||||
{
|
||||
obj.currentCount = obj.targetCount;
|
||||
obj.isCompleted = true;
|
||||
Debug.Log($"[QuestManager] Objective Completed: {obj.description}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
obj.isCompleted = true;
|
||||
Debug.Log($"[QuestManager] Objective Completed: {obj.description}");
|
||||
}
|
||||
|
||||
CheckQuestCompletion();
|
||||
NotifyUI();
|
||||
return true; // Successfully progressed!
|
||||
}
|
||||
|
||||
// If it didn't match, we still stop here because we only allow
|
||||
// progressing the CURRENT active objective.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a specific objective is the CURRENT active objective the player needs to do.
|
||||
/// </summary>
|
||||
public bool IsObjectiveActive(ObjectiveType type, string targetID)
|
||||
{
|
||||
if (activeQuest == null || activeQuest.isCompleted) return false;
|
||||
|
||||
foreach (var obj in activeQuest.objectives)
|
||||
{
|
||||
if (!obj.isCompleted)
|
||||
{
|
||||
// We found the FIRST incomplete objective! Is it this one?
|
||||
return (obj.type == type && obj.targetID == targetID);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void CheckQuestCompletion()
|
||||
{
|
||||
foreach (var obj in activeQuest.objectives)
|
||||
{
|
||||
if (!obj.isCompleted) return; // Still have objectives left
|
||||
}
|
||||
|
||||
activeQuest.isCompleted = true;
|
||||
Debug.Log($"[QuestManager] Quest Completed: {activeQuest.questName}");
|
||||
}
|
||||
|
||||
public void NotifyUI()
|
||||
{
|
||||
onQuestUpdated?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An NPC or Item can call this to give the player a brand new quest!
|
||||
/// </summary>
|
||||
public void StartNewQuest(Quest newQuest)
|
||||
{
|
||||
activeQuest = newQuest;
|
||||
NotifyUI();
|
||||
Debug.Log($"[QuestManager] Started new quest: {newQuest.questName}");
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/QuestManager.cs.meta
Normal file
2
Assets/Scripts/QuestManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 36a92c66f6c2b1b66a9223d539279aea
|
||||
63
Assets/Scripts/QuestObjectiveUI.cs
Normal file
63
Assets/Scripts/QuestObjectiveUI.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
using TMPro;
|
||||
|
||||
public class QuestObjectiveUI : MonoBehaviour
|
||||
{
|
||||
[Tooltip("The text component for the objective description")]
|
||||
public TextMeshProUGUI objectiveText;
|
||||
|
||||
[Header("GameObject Toggling (Option A)")]
|
||||
[Tooltip("The GameObject to show when the objective is NOT the currently active one (Completed or Pending future)")]
|
||||
[UnityEngine.Serialization.FormerlySerializedAs("incompleteIcon")]
|
||||
public GameObject inactiveIcon;
|
||||
|
||||
[Tooltip("The GameObject to show when the objective IS the currently active one")]
|
||||
[UnityEngine.Serialization.FormerlySerializedAs("completeIcon")]
|
||||
public GameObject activeIcon;
|
||||
|
||||
[Header("Sprite Swapping (Option B)")]
|
||||
[Tooltip("Assign this if you use a SINGLE Image and want to swap its sprite")]
|
||||
public Image iconImage;
|
||||
[Tooltip("Sprite for Completed/Pending objective")]
|
||||
public Sprite inactiveSprite;
|
||||
[Tooltip("Sprite for the Active objective")]
|
||||
public Sprite activeSprite;
|
||||
|
||||
public void Setup(string description, bool isActive, bool isCompleted, string countText = "")
|
||||
{
|
||||
if (objectiveText != null)
|
||||
{
|
||||
objectiveText.text = description + countText;
|
||||
|
||||
if (isCompleted)
|
||||
{
|
||||
// Completed
|
||||
objectiveText.fontStyle = FontStyles.Strikethrough;
|
||||
objectiveText.color = new Color(0.5f, 0.5f, 0.5f, 0.5f); // Faded gray
|
||||
}
|
||||
else if (isActive)
|
||||
{
|
||||
// Currently Active!
|
||||
objectiveText.fontStyle = FontStyles.Normal;
|
||||
objectiveText.color = Color.white;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Pending/Locked future objective
|
||||
objectiveText.fontStyle = FontStyles.Normal;
|
||||
objectiveText.color = Color.gray;
|
||||
}
|
||||
}
|
||||
|
||||
// Option A: Toggle GameObjects based on Completion
|
||||
if (inactiveIcon != null) inactiveIcon.SetActive(!isCompleted);
|
||||
if (activeIcon != null) activeIcon.SetActive(isCompleted);
|
||||
|
||||
// Option B: Swap Sprite on a single Image based on Active state
|
||||
if (iconImage != null)
|
||||
{
|
||||
iconImage.sprite = isActive ? activeSprite : inactiveSprite;
|
||||
}
|
||||
}
|
||||
}
|
||||
2
Assets/Scripts/QuestObjectiveUI.cs.meta
Normal file
2
Assets/Scripts/QuestObjectiveUI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a4faccd503c00e6ab8a3b2a55b6b674
|
||||
Reference in New Issue
Block a user