first commit
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d7afcdfc802a79c7cab8ff37fef4b004
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(menuName = "Lab05/Dialogue Data", fileName = "DialogueData")]
|
||||
public class DialogueData : ScriptableObject
|
||||
{
|
||||
public string npcName;
|
||||
[TextArea(2, 5)] public string greeting;
|
||||
public List<DialogueResponse> responses = new List<DialogueResponse>();
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class DialogueResponse
|
||||
{
|
||||
public string text;
|
||||
public QuestData questToStart;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 18a5c286e623a69a48c6546245b2b2c3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,81 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class NPCDialogue : MonoBehaviour
|
||||
{
|
||||
public string npcId = "VillageElder";
|
||||
public DialogueData dialogueData;
|
||||
public float interactionRange = 3f;
|
||||
public KeyCode interactionKey = KeyCode.E;
|
||||
|
||||
private Transform player;
|
||||
private bool promptVisible;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
GameObject playerObject = GameObject.FindGameObjectWithTag("Player");
|
||||
if (playerObject != null)
|
||||
{
|
||||
player = playerObject.transform;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (dialogueData == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (player == null)
|
||||
{
|
||||
GameObject playerObject = GameObject.FindGameObjectWithTag("Player");
|
||||
player = playerObject != null ? playerObject.transform : null;
|
||||
}
|
||||
|
||||
bool inRange = player != null && Vector3.Distance(player.position, transform.position) <= interactionRange;
|
||||
bool dialogueOpen = DialogueUI.Instance != null && DialogueUI.Instance.IsOpen;
|
||||
|
||||
if (!inRange || dialogueOpen)
|
||||
{
|
||||
HidePrompt();
|
||||
return;
|
||||
}
|
||||
|
||||
ShowPrompt();
|
||||
|
||||
if (Input.GetKeyDown(interactionKey) && DialogueUI.Instance != null)
|
||||
{
|
||||
HidePrompt();
|
||||
QuestManager.Instance?.OnNpcTalked(npcId);
|
||||
DialogueUI.Instance.Open(this, dialogueData);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
HidePrompt();
|
||||
}
|
||||
|
||||
private void ShowPrompt()
|
||||
{
|
||||
if (promptVisible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
promptVisible = true;
|
||||
string npcName = dialogueData != null ? dialogueData.npcName : "NPC";
|
||||
InteractionPromptUI.Instance?.Show($"E Поговорить: {npcName}");
|
||||
}
|
||||
|
||||
private void HidePrompt()
|
||||
{
|
||||
if (!promptVisible)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
promptVisible = false;
|
||||
InteractionPromptUI.Instance?.Hide();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 20689d635fb38dec6a942f3c78ba6e6a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fc0c5fc573f5df357979712014ebe319
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,48 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class Enemy : MonoBehaviour
|
||||
{
|
||||
public string enemyType = "Goblin";
|
||||
public int health = 100;
|
||||
public bool IsDead { get; private set; }
|
||||
|
||||
public void TakeDamage(int amount)
|
||||
{
|
||||
if (IsDead || amount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
health -= amount;
|
||||
if (health <= 0)
|
||||
{
|
||||
Die();
|
||||
}
|
||||
else if (NotificationUI.Instance != null)
|
||||
{
|
||||
NotificationUI.Instance.Show($"{enemyType}: {health} HP");
|
||||
}
|
||||
}
|
||||
|
||||
public void Die()
|
||||
{
|
||||
if (IsDead)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsDead = true;
|
||||
|
||||
if (QuestManager.Instance != null)
|
||||
{
|
||||
QuestManager.Instance.OnEnemyKilled(enemyType);
|
||||
}
|
||||
|
||||
if (NotificationUI.Instance != null)
|
||||
{
|
||||
NotificationUI.Instance.Show($"{enemyType} уничтожен");
|
||||
}
|
||||
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 117ef3366a8028ade8393946ba5eb300
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,56 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class PlayerCombat : MonoBehaviour
|
||||
{
|
||||
public float attackRange = 3f;
|
||||
public int damage = 100;
|
||||
public KeyCode keyboardAttack = KeyCode.F;
|
||||
|
||||
private void Update()
|
||||
{
|
||||
bool uiBlocksCombat =
|
||||
DialogueUI.Instance != null && DialogueUI.Instance.IsOpen ||
|
||||
QuestLogUI.Instance != null && QuestLogUI.Instance.IsOpen;
|
||||
|
||||
if (uiBlocksCombat)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Input.GetKeyDown(keyboardAttack) || Input.GetMouseButtonDown(0))
|
||||
{
|
||||
AttackNearestEnemy();
|
||||
}
|
||||
}
|
||||
|
||||
private void AttackNearestEnemy()
|
||||
{
|
||||
Enemy nearest = null;
|
||||
float nearestDistance = attackRange;
|
||||
Enemy[] enemies = FindObjectsOfType<Enemy>();
|
||||
|
||||
foreach (Enemy enemy in enemies)
|
||||
{
|
||||
if (enemy == null || enemy.IsDead)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
float distance = Vector3.Distance(transform.position, enemy.transform.position);
|
||||
if (distance <= nearestDistance)
|
||||
{
|
||||
nearestDistance = distance;
|
||||
nearest = enemy;
|
||||
}
|
||||
}
|
||||
|
||||
if (nearest != null)
|
||||
{
|
||||
nearest.TakeDamage(damage);
|
||||
}
|
||||
else if (NotificationUI.Instance != null)
|
||||
{
|
||||
NotificationUI.Instance.Show("Нет врага в радиусе атаки");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2dd80ac98d16da650a5885eadfb31917
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,62 @@
|
||||
using UnityEngine;
|
||||
|
||||
public class ResourceNode : MonoBehaviour
|
||||
{
|
||||
public ItemData itemData;
|
||||
public int amount = 1;
|
||||
public float interactionRange = 2.4f;
|
||||
public KeyCode interactionKey = KeyCode.E;
|
||||
|
||||
private Transform player;
|
||||
private bool collected;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
GameObject playerObject = GameObject.FindGameObjectWithTag("Player");
|
||||
if (playerObject != null)
|
||||
{
|
||||
player = playerObject.transform;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (collected || itemData == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (player == null)
|
||||
{
|
||||
GameObject playerObject = GameObject.FindGameObjectWithTag("Player");
|
||||
player = playerObject != null ? playerObject.transform : null;
|
||||
}
|
||||
|
||||
if (player == null || Vector3.Distance(player.position, transform.position) > interactionRange)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Input.GetKeyDown(interactionKey))
|
||||
{
|
||||
Collect();
|
||||
}
|
||||
}
|
||||
|
||||
private void Collect()
|
||||
{
|
||||
collected = true;
|
||||
|
||||
if (Inventory.Instance != null)
|
||||
{
|
||||
Inventory.Instance.AddItem(itemData, amount);
|
||||
}
|
||||
|
||||
if (NotificationUI.Instance != null)
|
||||
{
|
||||
NotificationUI.Instance.Show($"Собрано:\n{itemData.itemName} x{amount}");
|
||||
}
|
||||
|
||||
Destroy(gameObject);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 006e7c9e149f954dfa7124eb98d94f64
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,135 @@
|
||||
using UnityEngine;
|
||||
|
||||
[RequireComponent(typeof(CharacterController))]
|
||||
public class SimplePlayerController : MonoBehaviour
|
||||
{
|
||||
[Header("Movement")]
|
||||
public float moveSpeed = 5.5f;
|
||||
public float sprintSpeed = 7.5f;
|
||||
public float jumpHeight = 1.4f;
|
||||
public float gravity = -22f;
|
||||
public float rotationSmoothTime = 0.08f;
|
||||
|
||||
[Header("Camera")]
|
||||
public Transform cameraPivot;
|
||||
public Camera playerCamera;
|
||||
public float cameraDistance = 5.5f;
|
||||
public float mouseSensitivity = 2.2f;
|
||||
public float minPitch = -25f;
|
||||
public float maxPitch = 65f;
|
||||
|
||||
private CharacterController controller;
|
||||
private float verticalVelocity;
|
||||
private float yaw;
|
||||
private float pitch = 20f;
|
||||
private float rotationVelocity;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
controller = GetComponent<CharacterController>();
|
||||
|
||||
if (cameraPivot == null)
|
||||
{
|
||||
GameObject pivot = new GameObject("Camera Pivot");
|
||||
pivot.transform.SetParent(transform);
|
||||
pivot.transform.localPosition = new Vector3(0f, 1.45f, 0f);
|
||||
cameraPivot = pivot.transform;
|
||||
}
|
||||
|
||||
if (playerCamera == null)
|
||||
{
|
||||
playerCamera = Camera.main;
|
||||
}
|
||||
|
||||
if (playerCamera != null)
|
||||
{
|
||||
playerCamera.transform.SetParent(null);
|
||||
}
|
||||
|
||||
yaw = transform.eulerAngles.y;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
Cursor.lockState = CursorLockMode.Locked;
|
||||
Cursor.visible = false;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
bool uiBlocksMovement =
|
||||
DialogueUI.Instance != null && DialogueUI.Instance.IsOpen ||
|
||||
QuestLogUI.Instance != null && QuestLogUI.Instance.IsOpen;
|
||||
|
||||
if (uiBlocksMovement)
|
||||
{
|
||||
ApplyCamera();
|
||||
return;
|
||||
}
|
||||
|
||||
HandleLook();
|
||||
HandleMovement();
|
||||
ApplyCamera();
|
||||
}
|
||||
|
||||
private void HandleLook()
|
||||
{
|
||||
yaw += Input.GetAxis("Mouse X") * mouseSensitivity;
|
||||
pitch -= Input.GetAxis("Mouse Y") * mouseSensitivity;
|
||||
pitch = Mathf.Clamp(pitch, minPitch, maxPitch);
|
||||
}
|
||||
|
||||
private void HandleMovement()
|
||||
{
|
||||
bool grounded = controller.isGrounded;
|
||||
if (grounded && verticalVelocity < 0f)
|
||||
{
|
||||
verticalVelocity = -2f;
|
||||
}
|
||||
|
||||
Vector2 input = new Vector2(Input.GetAxisRaw("Horizontal"), Input.GetAxisRaw("Vertical"));
|
||||
input = Vector2.ClampMagnitude(input, 1f);
|
||||
|
||||
Vector3 cameraForward = Quaternion.Euler(0f, yaw, 0f) * Vector3.forward;
|
||||
Vector3 cameraRight = Quaternion.Euler(0f, yaw, 0f) * Vector3.right;
|
||||
Vector3 move = cameraForward * input.y + cameraRight * input.x;
|
||||
|
||||
if (move.sqrMagnitude > 0.001f)
|
||||
{
|
||||
float targetAngle = Mathf.Atan2(move.x, move.z) * Mathf.Rad2Deg;
|
||||
float smoothAngle = Mathf.SmoothDampAngle(transform.eulerAngles.y, targetAngle, ref rotationVelocity, rotationSmoothTime);
|
||||
transform.rotation = Quaternion.Euler(0f, smoothAngle, 0f);
|
||||
}
|
||||
|
||||
if (Input.GetButtonDown("Jump") && grounded)
|
||||
{
|
||||
verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity);
|
||||
}
|
||||
|
||||
verticalVelocity += gravity * Time.deltaTime;
|
||||
float speed = Input.GetKey(KeyCode.LeftShift) ? sprintSpeed : moveSpeed;
|
||||
Vector3 velocity = move * speed + Vector3.up * verticalVelocity;
|
||||
controller.Move(velocity * Time.deltaTime);
|
||||
}
|
||||
|
||||
private void ApplyCamera()
|
||||
{
|
||||
if (playerCamera == null || cameraPivot == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Quaternion rotation = Quaternion.Euler(pitch, yaw, 0f);
|
||||
Vector3 pivot = cameraPivot.position;
|
||||
Vector3 desiredPosition = pivot - rotation * Vector3.forward * cameraDistance;
|
||||
|
||||
if (Physics.Linecast(pivot, desiredPosition, out RaycastHit hit, ~0, QueryTriggerInteraction.Ignore)
|
||||
&& !hit.transform.IsChildOf(transform))
|
||||
{
|
||||
desiredPosition = hit.point + hit.normal * 0.25f;
|
||||
}
|
||||
|
||||
playerCamera.transform.position = desiredPosition;
|
||||
playerCamera.transform.rotation = rotation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1b485d09b14fd5b80b37a052d8ab0f7c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ece5c3bfe9095af99a1e249ee9a042ba
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class Inventory : MonoBehaviour
|
||||
{
|
||||
public static Inventory Instance { get; private set; }
|
||||
|
||||
[Serializable]
|
||||
public class InventorySlot
|
||||
{
|
||||
public ItemData item;
|
||||
public int amount;
|
||||
}
|
||||
|
||||
[SerializeField] private List<InventorySlot> slots = new List<InventorySlot>();
|
||||
|
||||
public IReadOnlyList<InventorySlot> Slots => slots;
|
||||
public event Action OnInventoryChanged;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
public void AddItem(ItemData item, int amount)
|
||||
{
|
||||
AddItem(item, amount, true);
|
||||
}
|
||||
|
||||
public void AddItem(ItemData item, int amount, bool notifyQuestManager)
|
||||
{
|
||||
if (item == null || amount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
InventorySlot slot = slots.Find(candidate => candidate.item == item);
|
||||
if (slot == null)
|
||||
{
|
||||
slot = new InventorySlot { item = item, amount = 0 };
|
||||
slots.Add(slot);
|
||||
}
|
||||
|
||||
slot.amount += amount;
|
||||
OnInventoryChanged?.Invoke();
|
||||
|
||||
if (notifyQuestManager && QuestManager.Instance != null)
|
||||
{
|
||||
QuestManager.Instance.OnItemCollected(item, amount);
|
||||
}
|
||||
}
|
||||
|
||||
public bool RemoveItem(ItemData item, int amount)
|
||||
{
|
||||
if (item == null || amount <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
InventorySlot slot = slots.Find(candidate => candidate.item == item);
|
||||
if (slot == null || slot.amount < amount)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
slot.amount -= amount;
|
||||
if (slot.amount <= 0)
|
||||
{
|
||||
slots.Remove(slot);
|
||||
}
|
||||
|
||||
OnInventoryChanged?.Invoke();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool HasItem(ItemData item, int amount)
|
||||
{
|
||||
return GetAmount(item) >= amount;
|
||||
}
|
||||
|
||||
public int GetAmount(ItemData item)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
InventorySlot slot = slots.Find(candidate => candidate.item == item);
|
||||
return slot == null ? 0 : slot.amount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a1bf7f2babca561069846dff84ddd247
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,11 @@
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(menuName = "Lab05/Item Data", fileName = "ItemData")]
|
||||
public class ItemData : ScriptableObject
|
||||
{
|
||||
public string itemId;
|
||||
public string itemName;
|
||||
[TextArea(2, 4)] public string description;
|
||||
public Sprite icon;
|
||||
[Min(1)] public int maxStack = 99;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf7df097b14a3255196cef81b5f93220
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4d8149501076fdd45aaebf3a6006c3db
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
[CreateAssetMenu(menuName = "Lab05/Quest Data", fileName = "QuestData")]
|
||||
public class QuestData : ScriptableObject
|
||||
{
|
||||
public string questId;
|
||||
public string questName;
|
||||
[TextArea(3, 6)] public string description;
|
||||
public List<QuestGoal> goals = new List<QuestGoal>();
|
||||
public ItemData rewardItem;
|
||||
[Min(0)] public int rewardAmount;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: dcd9bc81ab61a9eae896e42d1df1c2c8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
public enum QuestGoalType
|
||||
{
|
||||
CollectItem,
|
||||
KillEnemy,
|
||||
TalkToNPC
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class QuestGoal
|
||||
{
|
||||
public QuestGoalType goalType;
|
||||
public ItemData targetItem;
|
||||
public string enemyType;
|
||||
public string npcId;
|
||||
[Min(1)] public int requiredAmount = 1;
|
||||
|
||||
public string GetTargetKey()
|
||||
{
|
||||
switch (goalType)
|
||||
{
|
||||
case QuestGoalType.CollectItem:
|
||||
return targetItem != null ? targetItem.itemId : string.Empty;
|
||||
case QuestGoalType.KillEnemy:
|
||||
return enemyType;
|
||||
case QuestGoalType.TalkToNPC:
|
||||
return npcId;
|
||||
default:
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
public string GetDisplayName()
|
||||
{
|
||||
switch (goalType)
|
||||
{
|
||||
case QuestGoalType.CollectItem:
|
||||
return targetItem != null ? targetItem.itemName : "Unknown item";
|
||||
case QuestGoalType.KillEnemy:
|
||||
return string.IsNullOrWhiteSpace(enemyType) ? "Enemy" : enemyType;
|
||||
case QuestGoalType.TalkToNPC:
|
||||
return string.IsNullOrWhiteSpace(npcId) ? "NPC" : npcId;
|
||||
default:
|
||||
return "Goal";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c6edd48c91e2c16bd812db71cb6638bf
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,276 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
public class QuestManager : MonoBehaviour
|
||||
{
|
||||
public static QuestManager Instance { get; private set; }
|
||||
|
||||
[SerializeField] private List<QuestData> availableQuests = new List<QuestData>();
|
||||
[SerializeField] private List<QuestState> activeQuests = new List<QuestState>();
|
||||
[SerializeField] private List<QuestState> completedQuests = new List<QuestState>();
|
||||
[SerializeField] private List<QuestState> failedQuests = new List<QuestState>();
|
||||
|
||||
public IReadOnlyList<QuestData> AvailableQuests => availableQuests;
|
||||
public IReadOnlyList<QuestState> ActiveQuests => activeQuests;
|
||||
public IReadOnlyList<QuestState> CompletedQuests => completedQuests;
|
||||
public IReadOnlyList<QuestState> FailedQuests => failedQuests;
|
||||
|
||||
public event Action OnQuestsChanged;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
public void SetAvailableQuests(IEnumerable<QuestData> quests)
|
||||
{
|
||||
availableQuests.Clear();
|
||||
if (quests != null)
|
||||
{
|
||||
availableQuests.AddRange(quests);
|
||||
}
|
||||
}
|
||||
|
||||
public bool StartQuest(QuestData quest)
|
||||
{
|
||||
if (quest == null || IsQuestActive(quest) || IsQuestCompleted(quest))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!availableQuests.Contains(quest))
|
||||
{
|
||||
availableQuests.Add(quest);
|
||||
}
|
||||
|
||||
QuestState state = new QuestState(quest);
|
||||
activeQuests.Add(state);
|
||||
Notify($"Получен квест:\n{quest.questName}");
|
||||
OnQuestsChanged?.Invoke();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool CompleteQuest(QuestData quest)
|
||||
{
|
||||
QuestState state = activeQuests.Find(candidate => candidate.questData == quest);
|
||||
if (state == null || !state.AreAllGoalsComplete())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ConsumeRequiredItems(quest))
|
||||
{
|
||||
Notify($"Не хватает предметов для завершения:\n{quest.questName}");
|
||||
OnQuestsChanged?.Invoke();
|
||||
return false;
|
||||
}
|
||||
|
||||
state.status = QuestStatus.Completed;
|
||||
activeQuests.Remove(state);
|
||||
completedQuests.Add(state);
|
||||
|
||||
Notify($"Квест завершён:\n{quest.questName}");
|
||||
|
||||
if (quest.rewardItem != null && quest.rewardAmount > 0 && Inventory.Instance != null)
|
||||
{
|
||||
Inventory.Instance.AddItem(quest.rewardItem, quest.rewardAmount, false);
|
||||
Notify($"Получена награда:\n{quest.rewardItem.itemId} x{quest.rewardAmount}");
|
||||
}
|
||||
|
||||
OnQuestsChanged?.Invoke();
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool ConsumeRequiredItems(QuestData quest)
|
||||
{
|
||||
if (quest == null || quest.goals == null || Inventory.Instance == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (QuestGoal goal in quest.goals)
|
||||
{
|
||||
if (goal.goalType != QuestGoalType.CollectItem || goal.targetItem == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Inventory.Instance.HasItem(goal.targetItem, goal.requiredAmount))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (QuestGoal goal in quest.goals)
|
||||
{
|
||||
if (goal.goalType == QuestGoalType.CollectItem && goal.targetItem != null)
|
||||
{
|
||||
Inventory.Instance.RemoveItem(goal.targetItem, goal.requiredAmount);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void FailQuest(QuestData quest)
|
||||
{
|
||||
QuestState state = activeQuests.Find(candidate => candidate.questData == quest);
|
||||
if (state == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
state.status = QuestStatus.Failed;
|
||||
activeQuests.Remove(state);
|
||||
failedQuests.Add(state);
|
||||
OnQuestsChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void OnItemCollected(ItemData item, int amount)
|
||||
{
|
||||
if (item == null || amount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateProgress(QuestGoalType.CollectItem, item.itemId, amount);
|
||||
}
|
||||
|
||||
public void OnEnemyKilled(string enemyType)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(enemyType))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateProgress(QuestGoalType.KillEnemy, enemyType, 1);
|
||||
}
|
||||
|
||||
public void OnNpcTalked(string npcId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(npcId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateProgress(QuestGoalType.TalkToNPC, npcId, 1);
|
||||
}
|
||||
|
||||
public string GetGoalProgress(QuestData quest, int goalIndex)
|
||||
{
|
||||
QuestState state = GetQuestState(quest);
|
||||
if (state == null || quest == null || goalIndex < 0 || goalIndex >= quest.goals.Count)
|
||||
{
|
||||
return "0/0";
|
||||
}
|
||||
|
||||
QuestGoal goal = quest.goals[goalIndex];
|
||||
return $"{state.GetProgress(goalIndex)}/{goal.requiredAmount}";
|
||||
}
|
||||
|
||||
public float GetQuestCompletionPercent(QuestState state)
|
||||
{
|
||||
if (state == null || state.questData == null || state.questData.goals.Count == 0)
|
||||
{
|
||||
return 0f;
|
||||
}
|
||||
|
||||
float total = 0f;
|
||||
for (int i = 0; i < state.questData.goals.Count; i++)
|
||||
{
|
||||
QuestGoal goal = state.questData.goals[i];
|
||||
total += Mathf.Clamp01(state.GetProgress(i) / (float)Mathf.Max(1, goal.requiredAmount));
|
||||
}
|
||||
|
||||
return total / state.questData.goals.Count;
|
||||
}
|
||||
|
||||
public QuestState GetQuestState(QuestData quest)
|
||||
{
|
||||
QuestState state = activeQuests.Find(candidate => candidate.questData == quest);
|
||||
if (state != null)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
state = completedQuests.Find(candidate => candidate.questData == quest);
|
||||
if (state != null)
|
||||
{
|
||||
return state;
|
||||
}
|
||||
|
||||
return failedQuests.Find(candidate => candidate.questData == quest);
|
||||
}
|
||||
|
||||
private void UpdateProgress(QuestGoalType type, string targetKey, int amount)
|
||||
{
|
||||
bool changed = false;
|
||||
List<QuestData> completedNow = new List<QuestData>();
|
||||
|
||||
foreach (QuestState state in activeQuests)
|
||||
{
|
||||
QuestData quest = state.questData;
|
||||
if (quest == null || quest.goals == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
for (int i = 0; i < quest.goals.Count; i++)
|
||||
{
|
||||
QuestGoal goal = quest.goals[i];
|
||||
if (goal.goalType != type || goal.GetTargetKey() != targetKey || state.IsGoalComplete(i))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
state.AddProgress(i, amount);
|
||||
changed = true;
|
||||
Notify($"Прогресс:\n{goal.GetDisplayName()} {state.GetProgress(i)}/{goal.requiredAmount}");
|
||||
}
|
||||
|
||||
if (state.AreAllGoalsComplete())
|
||||
{
|
||||
completedNow.Add(quest);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (QuestData quest in completedNow)
|
||||
{
|
||||
CompleteQuest(quest);
|
||||
}
|
||||
|
||||
if (changed)
|
||||
{
|
||||
OnQuestsChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsQuestActive(QuestData quest)
|
||||
{
|
||||
return activeQuests.Exists(candidate => candidate.questData == quest);
|
||||
}
|
||||
|
||||
private bool IsQuestCompleted(QuestData quest)
|
||||
{
|
||||
return completedQuests.Exists(candidate => candidate.questData == quest);
|
||||
}
|
||||
|
||||
private void Notify(string message)
|
||||
{
|
||||
if (NotificationUI.Instance != null)
|
||||
{
|
||||
NotificationUI.Instance.Show(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e197b9d5258f0aa02850be4f5eb2db41
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,76 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
[Serializable]
|
||||
public class QuestState
|
||||
{
|
||||
public QuestData questData;
|
||||
public QuestStatus status;
|
||||
public List<int> goalProgress = new List<int>();
|
||||
|
||||
public QuestState(QuestData quest)
|
||||
{
|
||||
questData = quest;
|
||||
status = QuestStatus.Active;
|
||||
|
||||
int goalCount = quest != null && quest.goals != null ? quest.goals.Count : 0;
|
||||
for (int i = 0; i < goalCount; i++)
|
||||
{
|
||||
goalProgress.Add(0);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsGoalComplete(int index)
|
||||
{
|
||||
if (questData == null || index < 0 || index >= questData.goals.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return GetProgress(index) >= questData.goals[index].requiredAmount;
|
||||
}
|
||||
|
||||
public int GetProgress(int index)
|
||||
{
|
||||
if (index < 0 || index >= goalProgress.Count)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return goalProgress[index];
|
||||
}
|
||||
|
||||
public void AddProgress(int index, int amount)
|
||||
{
|
||||
if (questData == null || index < 0 || index >= questData.goals.Count || amount <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
while (goalProgress.Count < questData.goals.Count)
|
||||
{
|
||||
goalProgress.Add(0);
|
||||
}
|
||||
|
||||
int required = questData.goals[index].requiredAmount;
|
||||
goalProgress[index] = Math.Min(required, goalProgress[index] + amount);
|
||||
}
|
||||
|
||||
public bool AreAllGoalsComplete()
|
||||
{
|
||||
if (questData == null || questData.goals == null || questData.goals.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < questData.goals.Count; i++)
|
||||
{
|
||||
if (!IsGoalComplete(i))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5e3f862929be70f95ac2a633eabc57ec
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,6 @@
|
||||
public enum QuestStatus
|
||||
{
|
||||
Active,
|
||||
Completed,
|
||||
Failed
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e5ef75b1ddddcabb985c5c9d4ad4d89b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5c1fc8d1c958310ca99fd0732b27e34e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,108 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class DialogueUI : MonoBehaviour
|
||||
{
|
||||
public static DialogueUI Instance { get; private set; }
|
||||
|
||||
public GameObject root;
|
||||
public Text nameText;
|
||||
public Text bodyText;
|
||||
public RectTransform responseContainer;
|
||||
public Button responseButtonTemplate;
|
||||
|
||||
public bool IsOpen => root != null && root.activeSelf;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
Close();
|
||||
}
|
||||
|
||||
public void Open(NPCDialogue npc, DialogueData dialogueData)
|
||||
{
|
||||
if (root == null || dialogueData == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
root.SetActive(true);
|
||||
Cursor.lockState = CursorLockMode.None;
|
||||
Cursor.visible = true;
|
||||
|
||||
if (nameText != null)
|
||||
{
|
||||
nameText.text = dialogueData.npcName;
|
||||
}
|
||||
|
||||
if (bodyText != null)
|
||||
{
|
||||
bodyText.text = dialogueData.greeting;
|
||||
}
|
||||
|
||||
ClearResponses();
|
||||
|
||||
foreach (DialogueResponse response in dialogueData.responses)
|
||||
{
|
||||
Button button = Instantiate(responseButtonTemplate, responseContainer, false);
|
||||
button.name = $"Response - {response.text}";
|
||||
button.gameObject.SetActive(true);
|
||||
|
||||
Text text = button.GetComponentInChildren<Text>();
|
||||
if (text != null)
|
||||
{
|
||||
text.text = response.text;
|
||||
}
|
||||
|
||||
QuestData questToStart = response.questToStart;
|
||||
button.onClick.AddListener(() =>
|
||||
{
|
||||
if (questToStart != null && QuestManager.Instance != null)
|
||||
{
|
||||
QuestManager.Instance.StartQuest(questToStart);
|
||||
}
|
||||
|
||||
Close();
|
||||
});
|
||||
}
|
||||
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(responseContainer);
|
||||
}
|
||||
|
||||
public void Close()
|
||||
{
|
||||
if (root != null)
|
||||
{
|
||||
root.SetActive(false);
|
||||
}
|
||||
|
||||
ClearResponses();
|
||||
Cursor.lockState = CursorLockMode.Locked;
|
||||
Cursor.visible = false;
|
||||
}
|
||||
|
||||
private void ClearResponses()
|
||||
{
|
||||
if (responseContainer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = responseContainer.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
Transform child = responseContainer.GetChild(i);
|
||||
if (responseButtonTemplate != null && child == responseButtonTemplate.transform)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Destroy(child.gameObject);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 925b64e981cf522cfac0cba98e6500d8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,41 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class InteractionPromptUI : MonoBehaviour
|
||||
{
|
||||
public static InteractionPromptUI Instance { get; private set; }
|
||||
|
||||
public GameObject root;
|
||||
public Text promptText;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
Hide();
|
||||
}
|
||||
|
||||
public void Show(string message)
|
||||
{
|
||||
if (root == null || promptText == null || string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
promptText.text = message;
|
||||
root.SetActive(true);
|
||||
}
|
||||
|
||||
public void Hide()
|
||||
{
|
||||
if (root != null)
|
||||
{
|
||||
root.SetActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 52b3b86afc5b4dd77917ebe683003683
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,158 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class InventoryUI : MonoBehaviour
|
||||
{
|
||||
public RectTransform content;
|
||||
public Text outputText;
|
||||
public Font uiFont;
|
||||
public Color textColor = new Color(0.92f, 0.9f, 0.82f);
|
||||
|
||||
private float refreshTimer;
|
||||
private string lastSignature;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
Subscribe();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
Subscribe();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (Inventory.Instance != null)
|
||||
{
|
||||
Inventory.Instance.OnInventoryChanged -= Refresh;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
refreshTimer -= Time.unscaledDeltaTime;
|
||||
if (refreshTimer > 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
refreshTimer = 0.25f;
|
||||
Subscribe();
|
||||
|
||||
string signature = BuildSignature();
|
||||
if (signature != lastSignature)
|
||||
{
|
||||
lastSignature = signature;
|
||||
Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private void Subscribe()
|
||||
{
|
||||
if (Inventory.Instance != null)
|
||||
{
|
||||
Inventory.Instance.OnInventoryChanged -= Refresh;
|
||||
Inventory.Instance.OnInventoryChanged += Refresh;
|
||||
}
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
if (outputText != null)
|
||||
{
|
||||
outputText.text = BuildDisplayText();
|
||||
return;
|
||||
}
|
||||
|
||||
if (content == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearContent();
|
||||
|
||||
if (Inventory.Instance == null || Inventory.Instance.Slots.Count == 0)
|
||||
{
|
||||
CreateLine("Пусто");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Inventory.InventorySlot slot in Inventory.Instance.Slots)
|
||||
{
|
||||
if (slot.item == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
CreateLine($"{slot.item.itemName} x{slot.amount}");
|
||||
}
|
||||
}
|
||||
|
||||
private string BuildDisplayText()
|
||||
{
|
||||
if (Inventory.Instance == null || Inventory.Instance.Slots.Count == 0)
|
||||
{
|
||||
return "Пусто";
|
||||
}
|
||||
|
||||
System.Text.StringBuilder builder = new System.Text.StringBuilder();
|
||||
foreach (Inventory.InventorySlot slot in Inventory.Instance.Slots)
|
||||
{
|
||||
if (slot.item == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.AppendLine($"{slot.item.itemName} x{slot.amount}");
|
||||
}
|
||||
|
||||
return builder.Length > 0 ? builder.ToString().TrimEnd() : "Пусто";
|
||||
}
|
||||
|
||||
private string BuildSignature()
|
||||
{
|
||||
if (Inventory.Instance == null)
|
||||
{
|
||||
return "inventory:null";
|
||||
}
|
||||
|
||||
System.Text.StringBuilder builder = new System.Text.StringBuilder();
|
||||
foreach (Inventory.InventorySlot slot in Inventory.Instance.Slots)
|
||||
{
|
||||
if (slot.item == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.Append(slot.item.itemId).Append(':').Append(slot.amount).Append('|');
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private void ClearContent()
|
||||
{
|
||||
for (int i = content.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
Destroy(content.GetChild(i).gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateLine(string value)
|
||||
{
|
||||
GameObject line = new GameObject("Inventory Line", typeof(RectTransform), typeof(Text));
|
||||
line.transform.SetParent(content, false);
|
||||
|
||||
Text text = line.GetComponent<Text>();
|
||||
text.font = uiFont != null ? uiFont : Resources.GetBuiltinResource<Font>("Arial.ttf");
|
||||
text.fontSize = 16;
|
||||
text.color = textColor;
|
||||
text.text = value;
|
||||
|
||||
RectTransform rect = line.GetComponent<RectTransform>();
|
||||
rect.sizeDelta = new Vector2(0f, 24f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c9554462078ac6bc68e1dc1b149a5fe3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class NotificationUI : MonoBehaviour
|
||||
{
|
||||
public static NotificationUI Instance { get; private set; }
|
||||
|
||||
public Text messageText;
|
||||
public CanvasGroup canvasGroup;
|
||||
public float visibleTime = 2.8f;
|
||||
public float fadeTime = 0.35f;
|
||||
|
||||
private readonly Queue<string> queue = new Queue<string>();
|
||||
private float timer;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
if (canvasGroup == null)
|
||||
{
|
||||
canvasGroup = GetComponent<CanvasGroup>();
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (messageText == null || canvasGroup == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (timer > 0f)
|
||||
{
|
||||
timer -= Time.deltaTime;
|
||||
canvasGroup.alpha = timer < fadeTime ? Mathf.Clamp01(timer / fadeTime) : 1f;
|
||||
return;
|
||||
}
|
||||
|
||||
if (queue.Count > 0)
|
||||
{
|
||||
messageText.text = queue.Dequeue();
|
||||
timer = visibleTime + fadeTime;
|
||||
canvasGroup.alpha = 1f;
|
||||
}
|
||||
else
|
||||
{
|
||||
canvasGroup.alpha = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
public void Show(string message)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
queue.Enqueue(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6b3f91a3da3f8949696f87d49d1e45ed
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,208 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class QuestLogUI : MonoBehaviour
|
||||
{
|
||||
public static QuestLogUI Instance { get; private set; }
|
||||
|
||||
public GameObject root;
|
||||
public RectTransform content;
|
||||
public Font uiFont;
|
||||
public Color titleColor = new Color(1f, 0.74f, 0.25f);
|
||||
public Color textColor = new Color(0.92f, 0.9f, 0.82f);
|
||||
public Color mutedColor = new Color(0.7f, 0.7f, 0.7f);
|
||||
public KeyCode toggleKey = KeyCode.J;
|
||||
|
||||
public bool IsOpen => root != null && root.activeSelf;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
if (Instance != null && Instance != this)
|
||||
{
|
||||
Destroy(this);
|
||||
return;
|
||||
}
|
||||
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
if (root != null)
|
||||
{
|
||||
root.SetActive(false);
|
||||
}
|
||||
|
||||
Subscribe();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
Subscribe();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (QuestManager.Instance != null)
|
||||
{
|
||||
QuestManager.Instance.OnQuestsChanged -= Refresh;
|
||||
}
|
||||
}
|
||||
|
||||
private void Subscribe()
|
||||
{
|
||||
if (QuestManager.Instance != null)
|
||||
{
|
||||
QuestManager.Instance.OnQuestsChanged -= Refresh;
|
||||
QuestManager.Instance.OnQuestsChanged += Refresh;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (Input.GetKeyDown(toggleKey))
|
||||
{
|
||||
Toggle();
|
||||
}
|
||||
}
|
||||
|
||||
public void Toggle()
|
||||
{
|
||||
if (root == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool next = !root.activeSelf;
|
||||
root.SetActive(next);
|
||||
Cursor.lockState = next ? CursorLockMode.None : CursorLockMode.Locked;
|
||||
Cursor.visible = next;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
if (content == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearContent();
|
||||
|
||||
if (QuestManager.Instance == null)
|
||||
{
|
||||
CreateText("QuestManager не найден", mutedColor, 16, FontStyle.Normal);
|
||||
return;
|
||||
}
|
||||
|
||||
CreateText("Активные", titleColor, 22, FontStyle.Bold);
|
||||
if (QuestManager.Instance.ActiveQuests.Count == 0)
|
||||
{
|
||||
CreateText("Нет активных квестов", mutedColor, 16, FontStyle.Normal);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (QuestState state in QuestManager.Instance.ActiveQuests)
|
||||
{
|
||||
CreateQuestBlock(state);
|
||||
}
|
||||
}
|
||||
|
||||
CreateText("Завершённые", titleColor, 22, FontStyle.Bold);
|
||||
if (QuestManager.Instance.CompletedQuests.Count == 0)
|
||||
{
|
||||
CreateText("Пока пусто", mutedColor, 16, FontStyle.Normal);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (QuestState state in QuestManager.Instance.CompletedQuests)
|
||||
{
|
||||
CreateQuestBlock(state);
|
||||
}
|
||||
}
|
||||
|
||||
if (QuestManager.Instance.FailedQuests.Count > 0)
|
||||
{
|
||||
CreateText("Проваленные", titleColor, 22, FontStyle.Bold);
|
||||
foreach (QuestState state in QuestManager.Instance.FailedQuests)
|
||||
{
|
||||
CreateQuestBlock(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateQuestBlock(QuestState state)
|
||||
{
|
||||
if (state == null || state.questData == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
QuestData quest = state.questData;
|
||||
CreateText(quest.questName, titleColor, 18, FontStyle.Bold);
|
||||
CreateText(quest.description, textColor, 15, FontStyle.Normal);
|
||||
CreateText($"Статус: {state.status}", mutedColor, 14, FontStyle.Normal);
|
||||
|
||||
for (int i = 0; i < quest.goals.Count; i++)
|
||||
{
|
||||
QuestGoal goal = quest.goals[i];
|
||||
CreateText($"{goal.GetDisplayName()} {state.GetProgress(i)}/{goal.requiredAmount}", textColor, 15, FontStyle.Normal);
|
||||
}
|
||||
|
||||
float percent = QuestManager.Instance.GetQuestCompletionPercent(state);
|
||||
CreateText($"{BuildProgressBar(percent)} {Mathf.RoundToInt(percent * 100f)}%", titleColor, 15, FontStyle.Normal);
|
||||
CreateSpacer(8f);
|
||||
}
|
||||
|
||||
private string BuildProgressBar(float percent)
|
||||
{
|
||||
const int length = 10;
|
||||
int filled = Mathf.RoundToInt(Mathf.Clamp01(percent) * length);
|
||||
return new string('█', filled) + new string('░', length - filled);
|
||||
}
|
||||
|
||||
private void ClearContent()
|
||||
{
|
||||
for (int i = content.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
Destroy(content.GetChild(i).gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateSpacer(float height)
|
||||
{
|
||||
GameObject spacer = new GameObject("Quest Log Spacer", typeof(RectTransform));
|
||||
spacer.transform.SetParent(content, false);
|
||||
spacer.GetComponent<RectTransform>().sizeDelta = new Vector2(0f, height);
|
||||
}
|
||||
|
||||
private void CreateText(string value, Color color, int size, FontStyle style)
|
||||
{
|
||||
GameObject line = new GameObject("Quest Log Text", typeof(RectTransform), typeof(Text));
|
||||
line.transform.SetParent(content, false);
|
||||
|
||||
Text text = line.GetComponent<Text>();
|
||||
text.font = uiFont != null ? uiFont : Resources.GetBuiltinResource<Font>("Arial.ttf");
|
||||
text.fontSize = size;
|
||||
text.fontStyle = style;
|
||||
text.color = color;
|
||||
text.text = value;
|
||||
text.horizontalOverflow = HorizontalWrapMode.Wrap;
|
||||
text.verticalOverflow = VerticalWrapMode.Overflow;
|
||||
|
||||
RectTransform rect = line.GetComponent<RectTransform>();
|
||||
rect.sizeDelta = new Vector2(0f, Mathf.Max(size + 10f, EstimateHeight(value, size)));
|
||||
}
|
||||
|
||||
private float EstimateHeight(string value, int size)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return size + 8f;
|
||||
}
|
||||
|
||||
int lineCount = Mathf.Max(1, Mathf.CeilToInt(value.Length / 42f));
|
||||
return lineCount * (size + 8f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6ed352b24b6a3787aa32532fcde461b8
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,181 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class QuestTrackerUI : MonoBehaviour
|
||||
{
|
||||
public RectTransform content;
|
||||
public Text outputText;
|
||||
public Font uiFont;
|
||||
public Color titleColor = new Color(1f, 0.74f, 0.25f);
|
||||
public Color textColor = new Color(0.92f, 0.9f, 0.82f);
|
||||
|
||||
private float refreshTimer;
|
||||
private string lastSignature;
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
Subscribe();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
Subscribe();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
if (QuestManager.Instance != null)
|
||||
{
|
||||
QuestManager.Instance.OnQuestsChanged -= Refresh;
|
||||
}
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
refreshTimer -= Time.unscaledDeltaTime;
|
||||
if (refreshTimer > 0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
refreshTimer = 0.25f;
|
||||
Subscribe();
|
||||
|
||||
string signature = BuildSignature();
|
||||
if (signature != lastSignature)
|
||||
{
|
||||
lastSignature = signature;
|
||||
Refresh();
|
||||
}
|
||||
}
|
||||
|
||||
private void Subscribe()
|
||||
{
|
||||
if (QuestManager.Instance != null)
|
||||
{
|
||||
QuestManager.Instance.OnQuestsChanged -= Refresh;
|
||||
QuestManager.Instance.OnQuestsChanged += Refresh;
|
||||
}
|
||||
}
|
||||
|
||||
public void Refresh()
|
||||
{
|
||||
if (outputText != null)
|
||||
{
|
||||
outputText.text = BuildDisplayText();
|
||||
return;
|
||||
}
|
||||
|
||||
if (content == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClearContent();
|
||||
|
||||
if (QuestManager.Instance == null || QuestManager.Instance.ActiveQuests.Count == 0)
|
||||
{
|
||||
CreateLine("Нет активных квестов", textColor, 15);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (QuestState state in QuestManager.Instance.ActiveQuests)
|
||||
{
|
||||
if (state.questData == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
CreateLine(state.questData.questName, titleColor, 16);
|
||||
for (int i = 0; i < state.questData.goals.Count; i++)
|
||||
{
|
||||
QuestGoal goal = state.questData.goals[i];
|
||||
CreateLine($"{goal.GetDisplayName()} {state.GetProgress(i)}/{goal.requiredAmount}", textColor, 14);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string BuildDisplayText()
|
||||
{
|
||||
if (QuestManager.Instance == null || QuestManager.Instance.ActiveQuests.Count == 0)
|
||||
{
|
||||
return "Нет активных квестов";
|
||||
}
|
||||
|
||||
System.Text.StringBuilder builder = new System.Text.StringBuilder();
|
||||
foreach (QuestState state in QuestManager.Instance.ActiveQuests)
|
||||
{
|
||||
if (state.questData == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.AppendLine(state.questData.questName);
|
||||
for (int i = 0; i < state.questData.goals.Count; i++)
|
||||
{
|
||||
QuestGoal goal = state.questData.goals[i];
|
||||
builder.Append(" ")
|
||||
.Append(goal.GetDisplayName())
|
||||
.Append(' ')
|
||||
.Append(state.GetProgress(i))
|
||||
.Append('/')
|
||||
.Append(goal.requiredAmount)
|
||||
.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
return builder.Length > 0 ? builder.ToString().TrimEnd() : "Нет активных квестов";
|
||||
}
|
||||
|
||||
private string BuildSignature()
|
||||
{
|
||||
if (QuestManager.Instance == null)
|
||||
{
|
||||
return "quests:null";
|
||||
}
|
||||
|
||||
System.Text.StringBuilder builder = new System.Text.StringBuilder();
|
||||
foreach (QuestState state in QuestManager.Instance.ActiveQuests)
|
||||
{
|
||||
if (state.questData == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
builder.Append(state.questData.questId).Append(':').Append(state.status).Append(':');
|
||||
for (int i = 0; i < state.goalProgress.Count; i++)
|
||||
{
|
||||
builder.Append(state.goalProgress[i]).Append(',');
|
||||
}
|
||||
|
||||
builder.Append('|');
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private void ClearContent()
|
||||
{
|
||||
for (int i = content.childCount - 1; i >= 0; i--)
|
||||
{
|
||||
Destroy(content.GetChild(i).gameObject);
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateLine(string value, Color color, int size)
|
||||
{
|
||||
GameObject line = new GameObject("Quest Tracker Line", typeof(RectTransform), typeof(Text));
|
||||
line.transform.SetParent(content, false);
|
||||
|
||||
Text text = line.GetComponent<Text>();
|
||||
text.font = uiFont != null ? uiFont : Resources.GetBuiltinResource<Font>("Arial.ttf");
|
||||
text.fontSize = size;
|
||||
text.color = color;
|
||||
text.text = value;
|
||||
|
||||
RectTransform rect = line.GetComponent<RectTransform>();
|
||||
rect.sizeDelta = new Vector2(0f, size + 8f);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5589ed98cd9cd7c6a89980739d1515e0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user