Files
BABA_YAGA/Assets/Scripts/AI NPC/EnemyAI.cs

471 lines
17 KiB
C#
Raw Normal View History

2026-06-05 21:24:41 +07:00
using System;
2026-06-05 14:10:16 +07:00
using System.Collections;
2026-05-30 17:41:31 +07:00
using System.Collections.Generic;
using UnityEngine;
2026-06-05 17:16:11 +07:00
using UnityEngine.AI;
2026-06-05 18:46:19 +07:00
using System.Linq;
2026-06-05 22:24:16 +07:00
using UnityEngine.InputSystem;
2026-06-05 21:24:41 +07:00
using Random = UnityEngine.Random;
[Serializable]
public class DialogueResult { public string text; public float speedMod; public float suspicionMod; }
2026-05-30 17:41:31 +07:00
2026-06-05 18:46:19 +07:00
// Quy trình ưu tiên: Né đòn --> Bắn hạ (Artifact) --> Đuổi theo (Vector) --> Điều tra --> Nói chuyện --> Đi tuần
2026-06-05 14:10:16 +07:00
[RequireComponent(typeof(NavMeshAgent))]
[RequireComponent(typeof(Rigidbody))]
2026-05-30 17:41:31 +07:00
public class EnemyAI : MonoBehaviour
{
2026-06-05 18:46:19 +07:00
[Header("References")]
2026-05-30 17:41:31 +07:00
public Transform player;
2026-06-05 15:59:33 +07:00
private NavMeshAgent agent;
private Rigidbody rb;
private FieldOfView fov;
2026-06-03 13:42:09 +07:00
2026-06-05 18:46:19 +07:00
[Header("Movement Settings")]
2026-06-05 15:59:33 +07:00
public float moveSpeed = 3f;
2026-06-05 17:16:11 +07:00
public float rotateSpeed = 10f;
2026-06-05 18:46:19 +07:00
[Header("Patrol Settings")]
2026-06-05 22:24:16 +07:00
2026-06-05 17:16:11 +07:00
public float patrolWaitTime = 2f;
private float currentWaitTime = 0f;
2026-06-05 22:24:16 +07:00
public float patrolSpeed = 2.5f;
public float patrolRadius = 12f; // Bán kính của khu vực tuần tra ngẫu nhiên
2026-06-05 17:16:11 +07:00
2026-06-05 22:24:16 +07:00
private Vector3 startPosition;
2026-06-05 18:46:19 +07:00
[Header("Combat State")]
2026-06-03 13:42:09 +07:00
public bool playerHasArtifact;
public GameObject laserPrefab;
public Transform firePoint;
2026-06-05 22:24:16 +07:00
public float minShootDelay = 1.5f; // Delay giữa các LOẠT BẮN
public float maxShootDelay = 3.5f;
2026-06-05 15:59:33 +07:00
private float nextShootTime;
2026-06-03 13:42:09 +07:00
2026-06-05 18:46:19 +07:00
[Header("Dodge Settings")]
2026-06-05 16:06:59 +07:00
public float dodgeForce = 10f;
public float dodgeDuration = 0.2f;
public float dodgeCooldown = 1.2f;
2026-06-05 14:10:16 +07:00
private bool isDodging = false;
2026-06-05 17:16:11 +07:00
private float nextDodgeTime = 0f;
2026-06-05 22:24:16 +07:00
[Header("Artifact Combat Upgrades (New)")]
[Tooltip("Khoảng cách di chuyển trái/phải ngẫu nhiên qua thời gian duy trì")]
public float minStrafeDuration = 0.5f;
public float maxStrafeDuration = 2.2f;
[Tooltip("Độ lệch tâm bắn (Độ). Số càng nhỏ bắn càng chuẩn, số lớn bắn càng lệch")]
public float maxSpreadAngle = 6f;
[Tooltip("Tốc độ bắn giữa các viên trong cùng 1 loạt đạn")]
public float burstInterval = 0.12f;
private float nextStrafeChangeTime;
private int strafeDirectionSign = 1; // -1: Trái, 1: Phải, 0: Đứng im bắn
private bool isShootingBurst = false; // Khóa chống trùng lặp loạt bắn
2026-06-05 18:46:19 +07:00
[Header("Conversation Settings")]
2026-06-05 17:16:11 +07:00
public string npcName = "Guard";
2026-06-05 21:24:41 +07:00
[TextArea] public string persona = "You are a bored security guard. You love coffee and hate night shifts.";
public float talkRange = 12f;
public float talkCooldown = 60f;
2026-06-05 17:16:11 +07:00
private float lastTalkTime;
2026-06-05 18:46:19 +07:00
public bool isTalking; // Public để debug
2026-06-05 17:16:11 +07:00
private EnemyAI talkingPartner;
private Hallucinate.UI.ChatBubble chatBubble;
2026-06-05 21:24:41 +07:00
[Header("Suspicion Settings")]
public float suspicionLevel = 0f;
public float investigationThreshold = 30f;
public float alertNeighborsThreshold = 70f;
public float alertRange = 20f;
2026-06-05 17:16:11 +07:00
public Node rootNode;
void Start()
2026-05-30 17:41:31 +07:00
{
2026-06-04 15:41:01 +07:00
agent = GetComponent<NavMeshAgent>();
2026-06-05 14:10:16 +07:00
rb = GetComponent<Rigidbody>();
2026-06-05 15:59:33 +07:00
fov = GetComponent<FieldOfView>();
2026-06-05 18:46:19 +07:00
chatBubble = GetComponentInChildren<Hallucinate.UI.ChatBubble>(true);
2026-06-04 23:01:39 +07:00
2026-06-05 17:16:11 +07:00
rb.isKinematic = true;
2026-06-05 15:59:33 +07:00
rb.freezeRotation = true;
2026-06-05 17:16:11 +07:00
if (player == null)
{
GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
if (playerObj != null) player = playerObj.transform;
}
2026-06-05 18:46:19 +07:00
2026-06-05 17:16:11 +07:00
InitTree();
2026-06-05 14:10:16 +07:00
}
2026-06-05 17:16:11 +07:00
void InitTree()
2026-05-30 17:41:31 +07:00
{
2026-06-05 18:46:19 +07:00
var dodgeSequence = new Sequence(new List<Node> { new TaskNode(CheckDodgeConditions), new TaskNode(ActionDodge) });
var laserSequence = new Sequence(new List<Node> { new TaskNode(CheckHasArtifact), new TaskNode(ActionFocusAndShoot) });
var chaseSequence = new Sequence(new List<Node> { new TaskNode(CheckCanSeePlayer), new TaskNode(ActionChasePlayer) });
var investigateSequence = new Sequence(new List<Node> { new TaskNode(CheckHasInvestigateTarget), new TaskNode(ActionInvestigate) });
var talkSequence = new Sequence(new List<Node> { new TaskNode(CheckCanTalkToNPC), new TaskNode(ActionTalk) });
2026-06-05 17:16:11 +07:00
var patrolAction = new TaskNode(ActionPatrol);
2026-06-03 13:42:09 +07:00
2026-06-05 17:16:11 +07:00
rootNode = new Selector(new List<Node>
2026-05-30 17:41:31 +07:00
{
2026-06-05 17:16:11 +07:00
dodgeSequence,
2026-06-03 13:42:09 +07:00
laserSequence,
2026-06-05 17:16:11 +07:00
chaseSequence,
investigateSequence,
talkSequence,
patrolAction
2026-05-30 17:41:31 +07:00
});
}
2026-06-05 17:16:11 +07:00
void Update()
{
if (player == null) return;
2026-06-05 18:46:19 +07:00
2026-06-05 21:24:41 +07:00
if (!agent.isOnNavMesh) return;
// Decay suspicion
suspicionLevel = Mathf.Max(0, suspicionLevel - Time.deltaTime * 0.5f);
2026-06-05 18:46:19 +07:00
if (!isTalking && !isDodging && agent.isStopped)
agent.isStopped = false;
2026-06-05 17:16:11 +07:00
rootNode?.Evaluate();
}
2026-06-05 15:59:33 +07:00
2026-06-05 18:46:19 +07:00
#region CONDITIONS
2026-06-05 17:16:11 +07:00
private NodeState CheckDodgeConditions()
2026-06-05 15:59:33 +07:00
{
2026-06-05 22:24:16 +07:00
if (playerHasArtifact) return NodeState.Failure; // Có cổ vật -> Không Dash né nữa
2026-06-05 17:16:11 +07:00
if (isDodging) return NodeState.Success;
2026-06-05 22:24:16 +07:00
if (fov != null && fov.canSeePlayer && Mouse.current.leftButton.isPressed)
2026-06-05 17:16:11 +07:00
return NodeState.Success;
2026-06-05 15:59:33 +07:00
return NodeState.Failure;
}
2026-06-03 13:42:09 +07:00
private NodeState CheckHasArtifact()
{
2026-06-05 17:16:11 +07:00
if (playerHasArtifact) StopConversation();
2026-06-04 15:41:01 +07:00
return playerHasArtifact ? NodeState.Success : NodeState.Failure;
2026-06-03 13:42:09 +07:00
}
private NodeState CheckCanSeePlayer()
{
2026-06-05 18:46:19 +07:00
bool canSee = fov != null && fov.canSeePlayer;
2026-06-05 21:24:41 +07:00
if (canSee) { StopConversation(); AlertNeighbors(); suspicionLevel = 100; }
2026-06-05 18:46:19 +07:00
return canSee ? NodeState.Success : NodeState.Failure;
2026-06-05 14:10:16 +07:00
}
2026-06-03 13:42:09 +07:00
2026-06-05 15:59:33 +07:00
private NodeState CheckHasInvestigateTarget()
2026-06-05 14:10:16 +07:00
{
2026-06-05 21:24:41 +07:00
if (fov != null && fov.lastKnownPlayerPosition != Vector3.zero)
{
if (suspicionLevel > investigationThreshold)
{
if (Random.value < (suspicionLevel / 100f)) return NodeState.Success;
}
}
return NodeState.Failure;
2026-06-05 17:16:11 +07:00
}
2026-06-03 13:42:09 +07:00
2026-06-05 17:16:11 +07:00
private NodeState CheckCanTalkToNPC()
2026-05-30 17:41:31 +07:00
{
2026-06-05 18:46:19 +07:00
if (playerHasArtifact || (fov != null && fov.canSeePlayer)) return NodeState.Failure;
2026-06-05 17:16:11 +07:00
if (Time.time < lastTalkTime + talkCooldown) return NodeState.Failure;
if (isTalking) return NodeState.Success;
2026-06-05 21:24:41 +07:00
if (Hallucinate.AI.ConversationManager.Instance == null)
{
Debug.LogError($"[AI {npcName}] ConversationManager Instance is NULL!");
return NodeState.Failure;
}
if (!Hallucinate.AI.ConversationManager.Instance.CanStartConversation()) return NodeState.Failure;
2026-06-05 17:16:11 +07:00
Collider[] hitColliders = Physics.OverlapSphere(transform.position, talkRange);
foreach (var hit in hitColliders)
2026-06-05 14:10:16 +07:00
{
2026-06-05 18:46:19 +07:00
if (hit.gameObject == gameObject) continue;
EnemyAI other = hit.GetComponentInParent<EnemyAI>();
if (other != null && !other.isTalking && Time.time >= other.lastTalkTime + talkCooldown)
2026-06-05 15:59:33 +07:00
{
2026-06-05 21:24:41 +07:00
float dist = Vector3.Distance(transform.position, other.transform.position);
if (dist <= talkRange && gameObject.GetInstanceID() < other.gameObject.GetInstanceID())
2026-06-05 17:16:11 +07:00
{
2026-06-05 21:24:41 +07:00
Debug.Log($"<color=green>[AI {npcName}]</color> Found partner: {other.npcName}. Starting conversation.");
Hallucinate.AI.ConversationManager.Instance.StartConversation(this, other);
2026-06-05 17:16:11 +07:00
return NodeState.Success;
}
2026-06-05 15:59:33 +07:00
}
2026-06-04 15:41:01 +07:00
}
2026-06-05 17:16:11 +07:00
return NodeState.Failure;
}
2026-06-05 18:46:19 +07:00
#endregion
#region ACTIONS
2026-06-05 21:24:41 +07:00
public void HearNoise(Vector3 location, float volume)
{
suspicionLevel += volume * 15f;
if (fov != null) fov.lastKnownPlayerPosition = location;
if (suspicionLevel >= alertNeighborsThreshold) AlertNeighbors();
StopConversation();
Debug.Log($"<color=orange>[AI {npcName}]</color> Heard noise! Suspicion: {suspicionLevel}");
}
public void AlertNeighbors()
2026-06-05 17:16:11 +07:00
{
2026-06-05 21:24:41 +07:00
Collider[] hitColliders = Physics.OverlapSphere(transform.position, alertRange);
foreach (var hit in hitColliders)
2026-06-05 17:16:11 +07:00
{
2026-06-05 21:24:41 +07:00
EnemyAI neighbor = hit.GetComponentInParent<EnemyAI>();
if (neighbor != null && neighbor != this)
{
neighbor.suspicionLevel = Mathf.Max(neighbor.suspicionLevel, 50f);
if (fov != null && neighbor.fov != null) neighbor.fov.lastKnownPlayerPosition = fov.lastKnownPlayerPosition;
}
2026-06-05 17:16:11 +07:00
}
2026-05-30 17:41:31 +07:00
}
2026-06-05 21:24:41 +07:00
private NodeState ActionTalk()
2026-06-05 17:16:11 +07:00
{
2026-06-05 21:24:41 +07:00
if (isTalking)
{
agent.isStopped = true;
if (talkingPartner != null)
{
if (Vector3.Distance(transform.position, talkingPartner.transform.position) > talkRange + 2f)
{
Debug.Log($"[AI {npcName}] Partner moved too far. Ending conversation.");
StopConversation();
return NodeState.Failure;
}
}
return NodeState.Running;
}
return NodeState.Failure;
2026-06-05 17:16:11 +07:00
}
2026-06-05 21:24:41 +07:00
public void ProcessDialogueResult(string json)
2026-06-05 17:16:11 +07:00
{
2026-06-05 21:24:41 +07:00
try
{
DialogueResult result = JsonUtility.FromJson<DialogueResult>(json);
if (chatBubble != null) chatBubble.Show(result.text);
moveSpeed += result.speedMod;
suspicionLevel = Mathf.Clamp(suspicionLevel + result.suspicionMod, 0, 100);
lastTalkTime = Time.time;
}
catch { if (chatBubble != null) chatBubble.Show(json); }
2026-06-05 17:16:11 +07:00
}
private void StopConversation()
{
2026-06-05 21:24:41 +07:00
if (isTalking && Hallucinate.AI.ConversationManager.Instance != null)
{
Hallucinate.AI.ConversationManager.Instance.InterruptConversation(this);
if (chatBubble != null) chatBubble.Show("Wait, what was that?!", 2f);
}
2026-06-05 17:16:11 +07:00
}
2026-06-05 18:46:19 +07:00
private NodeState ActionPatrol()
2026-05-30 17:41:31 +07:00
{
2026-06-05 22:24:16 +07:00
Debug.Log("Wandering randomly...");
2026-06-05 18:46:19 +07:00
agent.isStopped = false;
2026-06-05 22:24:16 +07:00
agent.speed = patrolSpeed;
2026-06-03 13:42:09 +07:00
2026-06-05 22:24:16 +07:00
// Kiểm tra xem NPC đã đi đến điểm ngẫu nhiên hiện tại chưa
if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance)
2026-05-30 17:41:31 +07:00
{
2026-06-05 18:46:19 +07:00
currentWaitTime += Time.deltaTime;
2026-06-05 22:24:16 +07:00
// Đứng đợi hết thời gian quy định rồi mới tìm đường mới
2026-06-05 18:46:19 +07:00
if (currentWaitTime >= patrolWaitTime)
{
2026-06-05 22:24:16 +07:00
// 1. Lấy một điểm ngẫu nhiên trong không gian hình cầu dựa trên bán kính
Vector3 randomDirection = Random.insideUnitSphere * patrolRadius;
randomDirection += startPosition; // Cộng với tâm ban đầu để giới hạn khu vực
NavMeshHit hit;
// 2. Ép tọa độ ngẫu nhiên đó phải nằm TRÊN bề mặt xanh của NavMesh (tránh kẹt tường)
// Số 1 ở cuối là Area Mask (thường là Walkable)
if (NavMesh.SamplePosition(randomDirection, out hit, patrolRadius, 1))
{
agent.SetDestination(hit.position);
}
currentWaitTime = 0f; // Reset thời gian chờ
2026-06-05 18:46:19 +07:00
}
2026-05-30 17:41:31 +07:00
}
2026-06-05 22:24:16 +07:00
2026-06-03 13:42:09 +07:00
return NodeState.Running;
2026-05-30 17:41:31 +07:00
}
2026-06-03 13:42:09 +07:00
2026-06-05 18:46:19 +07:00
private NodeState ActionChasePlayer()
2026-06-03 13:42:09 +07:00
{
2026-06-05 18:46:19 +07:00
agent.isStopped = false;
agent.speed = moveSpeed;
agent.SetDestination(player.position);
return NodeState.Running;
2026-06-05 17:16:11 +07:00
}
private NodeState ActionInvestigate()
{
agent.isStopped = false;
agent.speed = moveSpeed * 0.7f;
agent.SetDestination(fov.lastKnownPlayerPosition);
2026-06-05 21:24:41 +07:00
2026-06-05 18:46:19 +07:00
if (Vector3.Distance(transform.position, fov.lastKnownPlayerPosition) < 1.5f)
2026-06-05 17:16:11 +07:00
{
2026-06-05 21:24:41 +07:00
currentWaitTime += Time.deltaTime;
2026-06-05 22:24:16 +07:00
if (currentWaitTime > 3f)
2026-06-05 21:24:41 +07:00
{
fov.lastKnownPlayerPosition = Vector3.zero;
2026-06-05 22:24:16 +07:00
suspicionLevel *= 0.5f;
2026-06-05 21:24:41 +07:00
return NodeState.Success;
}
2026-06-05 17:16:11 +07:00
}
return NodeState.Running;
}
2026-06-05 18:46:19 +07:00
private NodeState ActionFocusAndShoot()
2026-06-05 17:16:11 +07:00
{
2026-06-05 21:43:41 +07:00
if (player == null) return NodeState.Failure;
2026-06-05 22:24:16 +07:00
if (agent.hasPath) agent.ResetPath();
agent.isStopped = false;
2026-06-05 21:43:41 +07:00
2026-06-05 22:24:16 +07:00
// 1. XOAY THÂN THEO TRỤC NGANG HƯỚNG VỀ PLAYER
2026-06-05 21:43:41 +07:00
Vector3 bodyDir = player.position - transform.position;
2026-06-05 22:24:16 +07:00
bodyDir.y = 0f;
Vector3 bodyDirNormal = bodyDir.normalized;
2026-06-05 21:43:41 +07:00
if (bodyDir != Vector3.zero)
{
2026-06-05 22:24:16 +07:00
Quaternion targetRotation = Quaternion.LookRotation(bodyDirNormal);
2026-06-05 21:43:41 +07:00
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotateSpeed * Time.deltaTime);
}
2026-06-05 22:24:16 +07:00
// 2. RANDOM KHOẢNG CÁCH DI CHUYỂN TRÁI/PHẢI (Tính theo thời gian duy trì)
if (Time.time >= nextStrafeChangeTime)
{
int[] choices = { -1, 1, 0 }; // Trái, Phải, Đứng yên bắn
strafeDirectionSign = choices[Random.Range(0, choices.Length)];
// Ép khoảng cách di chuyển dài ngắn ngẫu nhiên bằng cách random thời gian đổi hướng
nextStrafeChangeTime = Time.time + Random.Range(minStrafeDuration, maxStrafeDuration);
}
if (strafeDirectionSign != 0 && bodyDir != Vector3.zero)
{
Vector3 strafeDir = new Vector3(-bodyDirNormal.z, 0, bodyDirNormal.x) * strafeDirectionSign;
agent.speed = moveSpeed * 0.75f;
agent.Move(strafeDir * agent.speed * Time.deltaTime);
}
// 3. XOAY HỌNG SÚNG TRỤC DỌC NHẮM VÀO NGƯỜI PLAYER
2026-06-05 21:43:41 +07:00
if (firePoint != null)
{
Vector3 targetCenter = player.position + Vector3.up * 1f;
Vector3 aimDir = targetCenter - firePoint.position;
if (aimDir != Vector3.zero)
{
firePoint.rotation = Quaternion.LookRotation(aimDir);
}
}
2026-06-05 22:24:16 +07:00
// 4. RANDOM SỐ ĐẠN (1-3) & RANDOM DELAY GIỮA CÁC ĐỢT BẮN
if (Time.time >= nextShootTime && !isShootingBurst)
2026-06-05 17:16:11 +07:00
{
2026-06-05 22:24:16 +07:00
int randomBulletCount = Random.Range(1, 4); // Trả về ngẫu nhiên 1, 2, hoặc 3 viên
StartCoroutine(ShootBurstRoutine(randomBulletCount));
// Cập nhật thời gian chờ cho loạt đạn tiếp theo (Random delay)
2026-06-05 18:46:19 +07:00
nextShootTime = Time.time + Random.Range(minShootDelay, maxShootDelay);
2026-06-05 17:16:11 +07:00
}
2026-06-05 21:43:41 +07:00
2026-06-05 18:46:19 +07:00
return NodeState.Running;
}
2026-06-05 22:24:16 +07:00
// Coroutine xử lý bắn loạt đạn kết hợp RANDOM ĐỘ LỆCH (Spread)
private IEnumerator ShootBurstRoutine(int bulletCount)
2026-06-05 21:43:41 +07:00
{
2026-06-05 22:24:16 +07:00
isShootingBurst = true;
for (int i = 0; i < bulletCount; i++)
{
if (laserPrefab == null || firePoint == null) break;
// Tính toán độ lệch ngẫu nhiên (Xoay quanh trục X và Y của họng súng để tạo độ "lệch tâm thông minh")
float randomX = Random.Range(-maxSpreadAngle, maxSpreadAngle);
float randomY = Random.Range(-maxSpreadAngle, maxSpreadAngle);
Quaternion spreadRotation = Quaternion.Euler(randomX, randomY, 0f);
// Nhân góc xoay gốc của họng súng với góc lệch ngẫu nhiên
Quaternion finalBulletRotation = firePoint.rotation * spreadRotation;
// Sinh đạn
Instantiate(laserPrefab, firePoint.position, finalBulletRotation);
Debug.Log($"<color=cyan>[AI Burst]</color> Viên thứ {i + 1}/{bulletCount} | Độ lệch X:{randomX:F1}, Y:{randomY:F1}");
// Nếu còn đạn trong loạt, đợi một khoảng ngắn (burstInterval) rồi mới bắn tiếp viên sau
if (i < bulletCount - 1)
{
yield return new WaitForSeconds(burstInterval);
}
}
isShootingBurst = false;
2026-06-05 21:43:41 +07:00
}
2026-06-05 22:24:16 +07:00
private void ShootLaser() { } // Hàm cũ không dùng nữa, đã có Burst lo
2026-06-05 18:46:19 +07:00
private NodeState ActionDodge()
{
if (!isDodging) StartCoroutine(DodgeRollRoutine());
return NodeState.Running;
}
2026-06-05 17:16:11 +07:00
2026-06-05 18:46:19 +07:00
private IEnumerator DodgeRollRoutine()
{
isDodging = true;
agent.enabled = false;
rb.isKinematic = false;
Vector3 dir = (player.position - transform.position).normalized;
Vector3 perp = new Vector3(-dir.z, 0, dir.x);
rb.AddForce((Random.value > 0.5f ? perp : -perp) * dodgeForce, ForceMode.Impulse);
yield return new WaitForSeconds(dodgeDuration);
rb.linearVelocity = Vector3.zero;
rb.isKinematic = true;
agent.enabled = true;
isDodging = false;
}
2026-06-05 17:16:11 +07:00
2026-06-05 22:24:16 +07:00
public void SetTalkingPartner(EnemyAI partner) { talkingPartner = partner; }
2026-06-05 21:24:41 +07:00
public void FaceTarget(Vector3 pos)
2026-06-05 18:46:19 +07:00
{
Vector3 dir = (pos - transform.position);
dir.y = 0;
if (dir != Vector3.zero) transform.rotation = Quaternion.LookRotation(dir);
2026-06-03 13:42:09 +07:00
}
#endregion
2026-06-05 18:46:19 +07:00
private void OnDrawGizmos()
{
Gizmos.color = Color.green;
Gizmos.DrawWireSphere(transform.position, talkRange);
if (isTalking && talkingPartner != null)
{
Gizmos.color = Color.yellow;
Gizmos.DrawLine(transform.position + Vector3.up, talkingPartner.transform.position + Vector3.up);
}
}
2026-06-05 22:24:16 +07:00
}