Merge branch 'main' of https://scove-vault.duckdns.org/scove/HALLUCINATION
This commit is contained in:
@@ -103,7 +103,7 @@ RectTransform:
|
|||||||
m_PrefabInstance: {fileID: 0}
|
m_PrefabInstance: {fileID: 0}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
m_GameObject: {fileID: 3381350595760429543}
|
m_GameObject: {fileID: 3381350595760429543}
|
||||||
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
|
m_LocalRotation: {x: 0, y: -1, z: 0, w: 0}
|
||||||
m_LocalPosition: {x: 0, y: 0, z: 0}
|
m_LocalPosition: {x: 0, y: 0, z: 0}
|
||||||
m_LocalScale: {x: 1, y: 1, z: 1}
|
m_LocalScale: {x: 1, y: 1, z: 1}
|
||||||
m_ConstrainProportionsScale: 0
|
m_ConstrainProportionsScale: 0
|
||||||
@@ -111,7 +111,7 @@ RectTransform:
|
|||||||
- {fileID: 6775114823217050358}
|
- {fileID: 6775114823217050358}
|
||||||
- {fileID: 2685789783496722106}
|
- {fileID: 2685789783496722106}
|
||||||
m_Father: {fileID: 6442306242859885696}
|
m_Father: {fileID: 6442306242859885696}
|
||||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
m_LocalEulerAnglesHint: {x: 0, y: -180, z: 0}
|
||||||
m_AnchorMin: {x: 0, y: 0}
|
m_AnchorMin: {x: 0, y: 0}
|
||||||
m_AnchorMax: {x: 0, y: 0}
|
m_AnchorMax: {x: 0, y: 0}
|
||||||
m_AnchoredPosition: {x: 0, y: 2.09}
|
m_AnchoredPosition: {x: 0, y: 2.09}
|
||||||
@@ -524,9 +524,13 @@ MonoBehaviour:
|
|||||||
dodgeCooldown: 3
|
dodgeCooldown: 3
|
||||||
npcName: Guard
|
npcName: Guard
|
||||||
persona: You are a grumpy guard protecting gold.
|
persona: You are a grumpy guard protecting gold.
|
||||||
talkRange: 20
|
talkRange: 5
|
||||||
talkCooldown: 30
|
talkCooldown: 30
|
||||||
isTalking: 0
|
isTalking: 0
|
||||||
|
suspicionLevel: 0
|
||||||
|
investigationThreshold: 30
|
||||||
|
alertNeighborsThreshold: 70
|
||||||
|
alertRange: 20
|
||||||
--- !u!195 &5770331367975928816
|
--- !u!195 &5770331367975928816
|
||||||
NavMeshAgent:
|
NavMeshAgent:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -669,7 +673,7 @@ PrefabInstance:
|
|||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 400132, guid: f32cd9f795c1282478d3bc3fbd8b2831, type: 3}
|
- target: {fileID: 400132, guid: f32cd9f795c1282478d3bc3fbd8b2831, type: 3}
|
||||||
propertyPath: m_LocalRotation.w
|
propertyPath: m_LocalRotation.w
|
||||||
value: 0
|
value: 1
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 400132, guid: f32cd9f795c1282478d3bc3fbd8b2831, type: 3}
|
- target: {fileID: 400132, guid: f32cd9f795c1282478d3bc3fbd8b2831, type: 3}
|
||||||
propertyPath: m_LocalRotation.x
|
propertyPath: m_LocalRotation.x
|
||||||
@@ -677,7 +681,7 @@ PrefabInstance:
|
|||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 400132, guid: f32cd9f795c1282478d3bc3fbd8b2831, type: 3}
|
- target: {fileID: 400132, guid: f32cd9f795c1282478d3bc3fbd8b2831, type: 3}
|
||||||
propertyPath: m_LocalRotation.y
|
propertyPath: m_LocalRotation.y
|
||||||
value: -1
|
value: 0
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 400132, guid: f32cd9f795c1282478d3bc3fbd8b2831, type: 3}
|
- target: {fileID: 400132, guid: f32cd9f795c1282478d3bc3fbd8b2831, type: 3}
|
||||||
propertyPath: m_LocalRotation.z
|
propertyPath: m_LocalRotation.z
|
||||||
@@ -689,7 +693,7 @@ PrefabInstance:
|
|||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 400132, guid: f32cd9f795c1282478d3bc3fbd8b2831, type: 3}
|
- target: {fileID: 400132, guid: f32cd9f795c1282478d3bc3fbd8b2831, type: 3}
|
||||||
propertyPath: m_LocalEulerAnglesHint.y
|
propertyPath: m_LocalEulerAnglesHint.y
|
||||||
value: -180
|
value: 0
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 400132, guid: f32cd9f795c1282478d3bc3fbd8b2831, type: 3}
|
- target: {fileID: 400132, guid: f32cd9f795c1282478d3bc3fbd8b2831, type: 3}
|
||||||
propertyPath: m_LocalEulerAnglesHint.z
|
propertyPath: m_LocalEulerAnglesHint.z
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
133
Assets/Scripts/AI NPC/ConversationManager.cs
Normal file
133
Assets/Scripts/AI NPC/ConversationManager.cs
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Hallucinate.AI
|
||||||
|
{
|
||||||
|
public class ConversationManager : MonoBehaviour
|
||||||
|
{
|
||||||
|
public static ConversationManager Instance { get; private set; }
|
||||||
|
|
||||||
|
[Header("Settings")]
|
||||||
|
public int maxSimultaneousConversations = 3;
|
||||||
|
public float maxConversationDuration = 120f; // 2 minutes
|
||||||
|
|
||||||
|
private List<ConversationSession> activeSessions = new List<ConversationSession>();
|
||||||
|
|
||||||
|
private void Awake()
|
||||||
|
{
|
||||||
|
if (Instance == null) Instance = this;
|
||||||
|
else Destroy(gameObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanStartConversation()
|
||||||
|
{
|
||||||
|
return activeSessions.Count < maxSimultaneousConversations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StartConversation(EnemyAI initiator, EnemyAI responder)
|
||||||
|
{
|
||||||
|
if (!CanStartConversation()) return;
|
||||||
|
|
||||||
|
ConversationSession session = new ConversationSession(initiator, responder, maxConversationDuration);
|
||||||
|
activeSessions.Add(session);
|
||||||
|
StartCoroutine(RunConversation(session));
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator RunConversation(ConversationSession session)
|
||||||
|
{
|
||||||
|
Debug.Log($"<color=cyan>[ConvManager]</color> Starting: {session.initiator.npcName} & {session.responder.npcName}");
|
||||||
|
|
||||||
|
// Phase 1: Initiator speaks
|
||||||
|
bool phase1Complete = false;
|
||||||
|
session.RequestDialogue(session.initiator, (success) => phase1Complete = true);
|
||||||
|
|
||||||
|
float startTime = Time.time;
|
||||||
|
while (!phase1Complete && Time.time < startTime + 10f) yield return null;
|
||||||
|
|
||||||
|
if (phase1Complete && !session.isInterrupted)
|
||||||
|
{
|
||||||
|
yield return new WaitForSeconds(4f); // Reading time
|
||||||
|
|
||||||
|
// Phase 2: Responder speaks
|
||||||
|
bool phase2Complete = false;
|
||||||
|
session.RequestDialogue(session.responder, (success) => phase2Complete = true);
|
||||||
|
|
||||||
|
float phase2StartTime = Time.time;
|
||||||
|
while (!phase2Complete && Time.time < phase2StartTime + 10f) yield return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return new WaitForSeconds(4f);
|
||||||
|
EndConversation(session);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EndConversation(ConversationSession session)
|
||||||
|
{
|
||||||
|
if (activeSessions.Contains(session))
|
||||||
|
{
|
||||||
|
session.Cleanup();
|
||||||
|
activeSessions.Remove(session);
|
||||||
|
Debug.Log($"<color=cyan>[ConvManager]</color> Ended session. Active: {activeSessions.Count}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InterruptConversation(EnemyAI npc)
|
||||||
|
{
|
||||||
|
ConversationSession session = activeSessions.Find(s => s.initiator == npc || s.responder == npc);
|
||||||
|
if (session != null)
|
||||||
|
{
|
||||||
|
session.isInterrupted = true;
|
||||||
|
EndConversation(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConversationSession
|
||||||
|
{
|
||||||
|
public EnemyAI initiator;
|
||||||
|
public EnemyAI responder;
|
||||||
|
public float durationLimit;
|
||||||
|
public bool isInterrupted;
|
||||||
|
|
||||||
|
public ConversationSession(EnemyAI initiator, EnemyAI responder, float limit)
|
||||||
|
{
|
||||||
|
this.initiator = initiator;
|
||||||
|
this.responder = responder;
|
||||||
|
this.durationLimit = limit;
|
||||||
|
|
||||||
|
initiator.isTalking = true;
|
||||||
|
responder.isTalking = true;
|
||||||
|
|
||||||
|
// Set references for Gizmos and Facing
|
||||||
|
initiator.SetTalkingPartner(responder);
|
||||||
|
responder.SetTalkingPartner(initiator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RequestDialogue(EnemyAI speaker, Action<bool> callback)
|
||||||
|
{
|
||||||
|
if (isInterrupted) { callback?.Invoke(false); return; }
|
||||||
|
|
||||||
|
EnemyAI listener = (speaker == initiator) ? responder : initiator;
|
||||||
|
|
||||||
|
// Face each other
|
||||||
|
speaker.FaceTarget(listener.transform.position);
|
||||||
|
listener.FaceTarget(speaker.transform.position);
|
||||||
|
|
||||||
|
string prompt = $"You are {speaker.npcName} talking to {listener.npcName}. Previous context: None. " +
|
||||||
|
"Keep it natural and short.";
|
||||||
|
|
||||||
|
GeminiService.Instance.GetResponse(speaker.persona, prompt, (json) => {
|
||||||
|
if (isInterrupted) { callback?.Invoke(false); return; }
|
||||||
|
speaker.ProcessDialogueResult(json);
|
||||||
|
callback?.Invoke(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Cleanup()
|
||||||
|
{
|
||||||
|
if (initiator != null) initiator.isTalking = false;
|
||||||
|
if (responder != null) responder.isTalking = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Scripts/AI NPC/ConversationManager.cs.meta
Normal file
2
Assets/Scripts/AI NPC/ConversationManager.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ebf63e5e8f429234b89a746833c4ca4e
|
||||||
@@ -1,8 +1,14 @@
|
|||||||
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
using UnityEngine.AI;
|
using UnityEngine.AI;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using UnityEngine.InputSystem;
|
||||||
|
using Random = UnityEngine.Random;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class DialogueResult { public string text; public float speedMod; public float suspicionMod; }
|
||||||
|
|
||||||
// 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
|
// 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
|
||||||
[RequireComponent(typeof(NavMeshAgent))]
|
[RequireComponent(typeof(NavMeshAgent))]
|
||||||
@@ -20,17 +26,21 @@ public class EnemyAI : MonoBehaviour
|
|||||||
public float rotateSpeed = 10f;
|
public float rotateSpeed = 10f;
|
||||||
|
|
||||||
[Header("Patrol Settings")]
|
[Header("Patrol Settings")]
|
||||||
public Transform[] patrolWaypoints;
|
|
||||||
public int currentWaypointIndex = 0;
|
|
||||||
public float patrolWaitTime = 2f;
|
public float patrolWaitTime = 2f;
|
||||||
private float currentWaitTime = 0f;
|
private float currentWaitTime = 0f;
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
private Vector3 startPosition;
|
||||||
[Header("Combat State")]
|
[Header("Combat State")]
|
||||||
public bool playerHasArtifact;
|
public bool playerHasArtifact;
|
||||||
public GameObject laserPrefab;
|
public GameObject laserPrefab;
|
||||||
public Transform firePoint;
|
public Transform firePoint;
|
||||||
public float minShootDelay = 1f;
|
public float minShootDelay = 1.5f; // Delay giữa các LOẠT BẮN
|
||||||
public float maxShootDelay = 3f;
|
public float maxShootDelay = 3.5f;
|
||||||
private float nextShootTime;
|
private float nextShootTime;
|
||||||
|
|
||||||
[Header("Dodge Settings")]
|
[Header("Dodge Settings")]
|
||||||
@@ -40,15 +50,34 @@ public class EnemyAI : MonoBehaviour
|
|||||||
private bool isDodging = false;
|
private bool isDodging = false;
|
||||||
private float nextDodgeTime = 0f;
|
private float nextDodgeTime = 0f;
|
||||||
|
|
||||||
|
[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
|
||||||
|
|
||||||
[Header("Conversation Settings")]
|
[Header("Conversation Settings")]
|
||||||
public string npcName = "Guard";
|
public string npcName = "Guard";
|
||||||
[TextArea] public string persona = "You are a grumpy guard protecting gold.";
|
[TextArea] public string persona = "You are a bored security guard. You love coffee and hate night shifts.";
|
||||||
public float talkRange = 10f;
|
public float talkRange = 12f;
|
||||||
public float talkCooldown = 15f;
|
public float talkCooldown = 60f;
|
||||||
private float lastTalkTime;
|
private float lastTalkTime;
|
||||||
public bool isTalking; // Public để debug
|
public bool isTalking; // Public để debug
|
||||||
private EnemyAI talkingPartner;
|
private EnemyAI talkingPartner;
|
||||||
private Hallucinate.UI.ChatBubble chatBubble;
|
private Hallucinate.UI.ChatBubble chatBubble;
|
||||||
|
|
||||||
|
[Header("Suspicion Settings")]
|
||||||
|
public float suspicionLevel = 0f;
|
||||||
|
public float investigationThreshold = 30f;
|
||||||
|
public float alertNeighborsThreshold = 70f;
|
||||||
|
public float alertRange = 20f;
|
||||||
|
|
||||||
public Node rootNode;
|
public Node rootNode;
|
||||||
|
|
||||||
@@ -59,7 +88,6 @@ public class EnemyAI : MonoBehaviour
|
|||||||
fov = GetComponent<FieldOfView>();
|
fov = GetComponent<FieldOfView>();
|
||||||
chatBubble = GetComponentInChildren<Hallucinate.UI.ChatBubble>(true);
|
chatBubble = GetComponentInChildren<Hallucinate.UI.ChatBubble>(true);
|
||||||
|
|
||||||
// Rigidbody setup cho Unity 6
|
|
||||||
rb.isKinematic = true;
|
rb.isKinematic = true;
|
||||||
rb.freezeRotation = true;
|
rb.freezeRotation = true;
|
||||||
|
|
||||||
@@ -69,18 +97,7 @@ public class EnemyAI : MonoBehaviour
|
|||||||
if (playerObj != null) player = playerObj.transform;
|
if (playerObj != null) player = playerObj.transform;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
// Tạm thời comment đoạn này để tránh lỗi Tag chưa định nghĩa
|
|
||||||
if (patrolWaypoints == null || patrolWaypoints.Length == 0)
|
|
||||||
{
|
|
||||||
patrolWaypoints = GameObject.FindGameObjectsWithTag("PatrolPoint")
|
|
||||||
.Select(go => go.transform).ToArray();
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
InitTree();
|
InitTree();
|
||||||
|
|
||||||
Debug.Log($"<color=white>[AI {npcName}] Init complete. Waypoints: {patrolWaypoints.Length}</color>");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void InitTree()
|
void InitTree()
|
||||||
@@ -107,12 +124,10 @@ public class EnemyAI : MonoBehaviour
|
|||||||
{
|
{
|
||||||
if (player == null) return;
|
if (player == null) return;
|
||||||
|
|
||||||
// An toàn cho NavMeshAgent
|
if (!agent.isOnNavMesh) return;
|
||||||
if (!agent.isOnNavMesh)
|
|
||||||
{
|
// Decay suspicion
|
||||||
Debug.LogWarning($"[AI {npcName}] NPC is NOT on NavMesh!");
|
suspicionLevel = Mathf.Max(0, suspicionLevel - Time.deltaTime * 0.5f);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isTalking && !isDodging && agent.isStopped)
|
if (!isTalking && !isDodging && agent.isStopped)
|
||||||
agent.isStopped = false;
|
agent.isStopped = false;
|
||||||
@@ -124,8 +139,10 @@ public class EnemyAI : MonoBehaviour
|
|||||||
|
|
||||||
private NodeState CheckDodgeConditions()
|
private NodeState CheckDodgeConditions()
|
||||||
{
|
{
|
||||||
|
if (playerHasArtifact) return NodeState.Failure; // Có cổ vật -> Không Dash né nữa
|
||||||
|
|
||||||
if (isDodging) return NodeState.Success;
|
if (isDodging) return NodeState.Success;
|
||||||
if (fov != null && fov.canSeePlayer && Input.GetMouseButtonDown(0) && Time.time >= nextDodgeTime)
|
if (fov != null && fov.canSeePlayer && Mouse.current.leftButton.isPressed)
|
||||||
return NodeState.Success;
|
return NodeState.Success;
|
||||||
return NodeState.Failure;
|
return NodeState.Failure;
|
||||||
}
|
}
|
||||||
@@ -139,13 +156,20 @@ public class EnemyAI : MonoBehaviour
|
|||||||
private NodeState CheckCanSeePlayer()
|
private NodeState CheckCanSeePlayer()
|
||||||
{
|
{
|
||||||
bool canSee = fov != null && fov.canSeePlayer;
|
bool canSee = fov != null && fov.canSeePlayer;
|
||||||
if (canSee) StopConversation();
|
if (canSee) { StopConversation(); AlertNeighbors(); suspicionLevel = 100; }
|
||||||
return canSee ? NodeState.Success : NodeState.Failure;
|
return canSee ? NodeState.Success : NodeState.Failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
private NodeState CheckHasInvestigateTarget()
|
private NodeState CheckHasInvestigateTarget()
|
||||||
{
|
{
|
||||||
return (fov != null && fov.lastKnownPlayerPosition != Vector3.zero) ? NodeState.Success : NodeState.Failure;
|
if (fov != null && fov.lastKnownPlayerPosition != Vector3.zero)
|
||||||
|
{
|
||||||
|
if (suspicionLevel > investigationThreshold)
|
||||||
|
{
|
||||||
|
if (Random.value < (suspicionLevel / 100f)) return NodeState.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NodeState.Failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
private NodeState CheckCanTalkToNPC()
|
private NodeState CheckCanTalkToNPC()
|
||||||
@@ -154,7 +178,14 @@ public class EnemyAI : MonoBehaviour
|
|||||||
if (Time.time < lastTalkTime + talkCooldown) return NodeState.Failure;
|
if (Time.time < lastTalkTime + talkCooldown) return NodeState.Failure;
|
||||||
if (isTalking) return NodeState.Success;
|
if (isTalking) return NodeState.Success;
|
||||||
|
|
||||||
// Quét tìm NPC
|
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;
|
||||||
|
|
||||||
Collider[] hitColliders = Physics.OverlapSphere(transform.position, talkRange);
|
Collider[] hitColliders = Physics.OverlapSphere(transform.position, talkRange);
|
||||||
foreach (var hit in hitColliders)
|
foreach (var hit in hitColliders)
|
||||||
{
|
{
|
||||||
@@ -163,10 +194,11 @@ public class EnemyAI : MonoBehaviour
|
|||||||
EnemyAI other = hit.GetComponentInParent<EnemyAI>();
|
EnemyAI other = hit.GetComponentInParent<EnemyAI>();
|
||||||
if (other != null && !other.isTalking && Time.time >= other.lastTalkTime + talkCooldown)
|
if (other != null && !other.isTalking && Time.time >= other.lastTalkTime + talkCooldown)
|
||||||
{
|
{
|
||||||
// Chỉ ID nhỏ hơn gọi để tránh trùng
|
float dist = Vector3.Distance(transform.position, other.transform.position);
|
||||||
if (gameObject.GetInstanceID() < other.gameObject.GetInstanceID())
|
if (dist <= talkRange && gameObject.GetInstanceID() < other.gameObject.GetInstanceID())
|
||||||
{
|
{
|
||||||
talkingPartner = other;
|
Debug.Log($"<color=green>[AI {npcName}]</color> Found partner: {other.npcName}. Starting conversation.");
|
||||||
|
Hallucinate.AI.ConversationManager.Instance.StartConversation(this, other);
|
||||||
return NodeState.Success;
|
return NodeState.Success;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,71 +210,101 @@ public class EnemyAI : MonoBehaviour
|
|||||||
|
|
||||||
#region ACTIONS
|
#region ACTIONS
|
||||||
|
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
Collider[] hitColliders = Physics.OverlapSphere(transform.position, alertRange);
|
||||||
|
foreach (var hit in hitColliders)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private NodeState ActionTalk()
|
private NodeState ActionTalk()
|
||||||
{
|
{
|
||||||
if (talkingPartner == null) return NodeState.Failure;
|
if (isTalking)
|
||||||
if (!isTalking)
|
|
||||||
{
|
{
|
||||||
isTalking = true;
|
|
||||||
agent.isStopped = true;
|
agent.isStopped = true;
|
||||||
FaceTarget(talkingPartner.transform.position);
|
if (talkingPartner != null)
|
||||||
|
{
|
||||||
Debug.Log($"<color=yellow>[AI {npcName}] Talking to {talkingPartner.npcName}</color>");
|
if (Vector3.Distance(transform.position, talkingPartner.transform.position) > talkRange + 2f)
|
||||||
|
{
|
||||||
string prompt = $"You are {npcName}. Speak 1 short sentence in English to your colleague {talkingPartner.npcName} about the shift.";
|
Debug.Log($"[AI {npcName}] Partner moved too far. Ending conversation.");
|
||||||
Hallucinate.AI.GeminiService.Instance.GetResponse(persona, prompt, (response) => {
|
StopConversation();
|
||||||
if (chatBubble != null) chatBubble.Show(response);
|
return NodeState.Failure;
|
||||||
Invoke(nameof(EndConversation), 5f);
|
}
|
||||||
});
|
}
|
||||||
talkingPartner.OnPartnerTalked(this);
|
return NodeState.Running;
|
||||||
}
|
}
|
||||||
return NodeState.Running;
|
return NodeState.Failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnPartnerTalked(EnemyAI partner)
|
public void ProcessDialogueResult(string json)
|
||||||
{
|
{
|
||||||
isTalking = true;
|
try
|
||||||
talkingPartner = partner;
|
{
|
||||||
agent.isStopped = true;
|
DialogueResult result = JsonUtility.FromJson<DialogueResult>(json);
|
||||||
FaceTarget(partner.transform.position);
|
if (chatBubble != null) chatBubble.Show(result.text);
|
||||||
Invoke(nameof(EndConversation), 6f);
|
|
||||||
}
|
moveSpeed += result.speedMod;
|
||||||
|
suspicionLevel = Mathf.Clamp(suspicionLevel + result.suspicionMod, 0, 100);
|
||||||
private void EndConversation()
|
lastTalkTime = Time.time;
|
||||||
{
|
}
|
||||||
isTalking = false;
|
catch { if (chatBubble != null) chatBubble.Show(json); }
|
||||||
lastTalkTime = Time.time;
|
|
||||||
if (agent != null && agent.isOnNavMesh) agent.isStopped = false;
|
|
||||||
talkingPartner = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void StopConversation()
|
private void StopConversation()
|
||||||
{
|
{
|
||||||
if (!isTalking) return;
|
if (isTalking && Hallucinate.AI.ConversationManager.Instance != null)
|
||||||
CancelInvoke(nameof(EndConversation));
|
{
|
||||||
EndConversation();
|
Hallucinate.AI.ConversationManager.Instance.InterruptConversation(this);
|
||||||
if (chatBubble != null) chatBubble.Show("Wait, what's that?!", 2f);
|
if (chatBubble != null) chatBubble.Show("Wait, what was that?!", 2f);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private NodeState ActionPatrol()
|
private NodeState ActionPatrol()
|
||||||
{
|
{
|
||||||
if (patrolWaypoints == null || patrolWaypoints.Length == 0) return NodeState.Failure;
|
Debug.Log("Wandering randomly...");
|
||||||
|
|
||||||
agent.isStopped = false;
|
agent.isStopped = false;
|
||||||
agent.speed = moveSpeed * 0.5f;
|
agent.speed = patrolSpeed;
|
||||||
|
|
||||||
var target = patrolWaypoints[currentWaypointIndex];
|
|
||||||
agent.SetDestination(target.position);
|
|
||||||
|
|
||||||
if (Vector3.Distance(transform.position, target.position) < 1.5f)
|
// 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)
|
||||||
{
|
{
|
||||||
currentWaitTime += Time.deltaTime;
|
currentWaitTime += Time.deltaTime;
|
||||||
|
|
||||||
|
// Đứng đợi hết thời gian quy định rồi mới tìm đường mới
|
||||||
if (currentWaitTime >= patrolWaitTime)
|
if (currentWaitTime >= patrolWaitTime)
|
||||||
{
|
{
|
||||||
currentWaypointIndex = (currentWaypointIndex + 1) % patrolWaypoints.Length;
|
// 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
|
||||||
currentWaitTime = 0f;
|
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ờ
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return NodeState.Running;
|
return NodeState.Running;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,26 +321,111 @@ public class EnemyAI : MonoBehaviour
|
|||||||
agent.isStopped = false;
|
agent.isStopped = false;
|
||||||
agent.speed = moveSpeed * 0.7f;
|
agent.speed = moveSpeed * 0.7f;
|
||||||
agent.SetDestination(fov.lastKnownPlayerPosition);
|
agent.SetDestination(fov.lastKnownPlayerPosition);
|
||||||
|
|
||||||
if (Vector3.Distance(transform.position, fov.lastKnownPlayerPosition) < 1.5f)
|
if (Vector3.Distance(transform.position, fov.lastKnownPlayerPosition) < 1.5f)
|
||||||
{
|
{
|
||||||
fov.lastKnownPlayerPosition = Vector3.zero;
|
currentWaitTime += Time.deltaTime;
|
||||||
return NodeState.Success;
|
if (currentWaitTime > 3f)
|
||||||
|
{
|
||||||
|
fov.lastKnownPlayerPosition = Vector3.zero;
|
||||||
|
suspicionLevel *= 0.5f;
|
||||||
|
return NodeState.Success;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return NodeState.Running;
|
return NodeState.Running;
|
||||||
}
|
}
|
||||||
|
|
||||||
private NodeState ActionFocusAndShoot()
|
private NodeState ActionFocusAndShoot()
|
||||||
{
|
{
|
||||||
agent.isStopped = true;
|
if (player == null) return NodeState.Failure;
|
||||||
FaceTarget(player.position);
|
|
||||||
if (Time.time >= nextShootTime)
|
if (agent.hasPath) agent.ResetPath();
|
||||||
|
agent.isStopped = false;
|
||||||
|
|
||||||
|
// 1. XOAY THÂN THEO TRỤC NGANG HƯỚNG VỀ PLAYER
|
||||||
|
Vector3 bodyDir = player.position - transform.position;
|
||||||
|
bodyDir.y = 0f;
|
||||||
|
Vector3 bodyDirNormal = bodyDir.normalized;
|
||||||
|
if (bodyDir != Vector3.zero)
|
||||||
{
|
{
|
||||||
if (laserPrefab && firePoint) Instantiate(laserPrefab, firePoint.position, firePoint.rotation);
|
Quaternion targetRotation = Quaternion.LookRotation(bodyDirNormal);
|
||||||
|
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotateSpeed * Time.deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
if (firePoint != null)
|
||||||
|
{
|
||||||
|
Vector3 targetCenter = player.position + Vector3.up * 1f;
|
||||||
|
Vector3 aimDir = targetCenter - firePoint.position;
|
||||||
|
if (aimDir != Vector3.zero)
|
||||||
|
{
|
||||||
|
firePoint.rotation = Quaternion.LookRotation(aimDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. RANDOM SỐ ĐẠN (1-3) & RANDOM DELAY GIỮA CÁC ĐỢT BẮN
|
||||||
|
if (Time.time >= nextShootTime && !isShootingBurst)
|
||||||
|
{
|
||||||
|
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)
|
||||||
nextShootTime = Time.time + Random.Range(minShootDelay, maxShootDelay);
|
nextShootTime = Time.time + Random.Range(minShootDelay, maxShootDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NodeState.Running;
|
return NodeState.Running;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Coroutine xử lý bắn loạt đạn kết hợp RANDOM ĐỘ LỆCH (Spread)
|
||||||
|
private IEnumerator ShootBurstRoutine(int bulletCount)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShootLaser() { } // Hàm cũ không dùng nữa, đã có Burst lo
|
||||||
|
|
||||||
private NodeState ActionDodge()
|
private NodeState ActionDodge()
|
||||||
{
|
{
|
||||||
if (!isDodging) StartCoroutine(DodgeRollRoutine());
|
if (!isDodging) StartCoroutine(DodgeRollRoutine());
|
||||||
@@ -300,7 +447,8 @@ public class EnemyAI : MonoBehaviour
|
|||||||
isDodging = false;
|
isDodging = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void FaceTarget(Vector3 pos)
|
public void SetTalkingPartner(EnemyAI partner) { talkingPartner = partner; }
|
||||||
|
public void FaceTarget(Vector3 pos)
|
||||||
{
|
{
|
||||||
Vector3 dir = (pos - transform.position);
|
Vector3 dir = (pos - transform.position);
|
||||||
dir.y = 0;
|
dir.y = 0;
|
||||||
@@ -311,15 +459,13 @@ public class EnemyAI : MonoBehaviour
|
|||||||
|
|
||||||
private void OnDrawGizmos()
|
private void OnDrawGizmos()
|
||||||
{
|
{
|
||||||
// Vẽ vùng nói chuyện (Xanh lá)
|
|
||||||
Gizmos.color = Color.green;
|
Gizmos.color = Color.green;
|
||||||
Gizmos.DrawWireSphere(transform.position, talkRange);
|
Gizmos.DrawWireSphere(transform.position, talkRange);
|
||||||
|
|
||||||
// Vẽ đường nối tới bạn diễn nếu đang nói
|
|
||||||
if (isTalking && talkingPartner != null)
|
if (isTalking && talkingPartner != null)
|
||||||
{
|
{
|
||||||
Gizmos.color = Color.yellow;
|
Gizmos.color = Color.yellow;
|
||||||
Gizmos.DrawLine(transform.position + Vector3.up, talkingPartner.transform.position + Vector3.up);
|
Gizmos.DrawLine(transform.position + Vector3.up, talkingPartner.transform.position + Vector3.up);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,9 +21,21 @@ namespace Hallucinate.AI
|
|||||||
public class GeminiService : MonoBehaviour
|
public class GeminiService : MonoBehaviour
|
||||||
{
|
{
|
||||||
public static GeminiService Instance { get; private set; }
|
public static GeminiService Instance { get; private set; }
|
||||||
|
private int activeRequests = 0;
|
||||||
|
private const int MAX_CONCURRENT_REQUESTS = 5;
|
||||||
|
|
||||||
[SerializeField] private string apiKey = "AQ.Ab8RN6I2hU_p8yHiPNNHtWzYBiLugbPP22gC6lzTWaYEWj4v0g"; // Replace with your key
|
[SerializeField] private string[] apiKeys = { "YOUR_KEY_1", "YOUR_KEY_2" };
|
||||||
[SerializeField] private string geminiURL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-latest:generateContent";
|
private int currentKeyIndex = 0;
|
||||||
|
|
||||||
|
[SerializeField] private string geminiURL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent";
|
||||||
|
private float nextRequestTime = 0f;
|
||||||
|
|
||||||
|
private string[] fallbackDialogues = {
|
||||||
|
"{ \"text\": \"Nice weather, isn't it?\", \"speedMod\": 0.0, \"suspicionMod\": -5.0 }",
|
||||||
|
"{ \"text\": \"Did you hear something? Probably just a rat.\", \"speedMod\": 0.0, \"suspicionMod\": 2.0 }",
|
||||||
|
"{ \"text\": \"I'm so tired of this shift.\", \"speedMod\": -0.1, \"suspicionMod\": 0.0 }",
|
||||||
|
"{ \"text\": \"Don't forget the coffee break later.\", \"speedMod\": 0.0, \"suspicionMod\": -2.0 }"
|
||||||
|
};
|
||||||
|
|
||||||
private void Awake()
|
private void Awake()
|
||||||
{
|
{
|
||||||
@@ -31,24 +43,50 @@ namespace Hallucinate.AI
|
|||||||
else { Destroy(gameObject); }
|
else { Destroy(gameObject); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string GetNextKey()
|
||||||
|
{
|
||||||
|
if (apiKeys == null || apiKeys.Length == 0) return "";
|
||||||
|
string key = apiKeys[currentKeyIndex];
|
||||||
|
currentKeyIndex = (currentKeyIndex + 1) % apiKeys.Length;
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
public void GetResponse(string persona, string prompt, Action<string> onComplete)
|
public void GetResponse(string persona, string prompt, Action<string> onComplete)
|
||||||
{
|
{
|
||||||
|
if (Time.time < nextRequestTime)
|
||||||
|
{
|
||||||
|
Debug.LogWarning("[Gemini] API is cooling down. Using fallback.");
|
||||||
|
onComplete?.Invoke(fallbackDialogues[UnityEngine.Random.Range(0, fallbackDialogues.Length)]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeRequests >= MAX_CONCURRENT_REQUESTS)
|
||||||
|
{
|
||||||
|
onComplete?.Invoke(fallbackDialogues[UnityEngine.Random.Range(0, fallbackDialogues.Length)]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
StartCoroutine(PostRequest(persona, prompt, onComplete));
|
StartCoroutine(PostRequest(persona, prompt, onComplete));
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerator PostRequest(string persona, string prompt, Action<string> onComplete)
|
private IEnumerator PostRequest(string persona, string prompt, Action<string> onComplete)
|
||||||
{
|
{
|
||||||
|
activeRequests++;
|
||||||
|
|
||||||
|
string jsonInstruction = " Respond ONLY with a JSON object: { 'text': 'your dialogue', 'speedMod': 0.0, 'suspicionMod': 0.0 }.";
|
||||||
|
string escapedPersona = persona.Replace("\"", "\\\"");
|
||||||
|
string escapedPrompt = prompt.Replace("\"", "\\\"");
|
||||||
|
|
||||||
var jsonBody = $@"{{
|
var jsonBody = $@"{{
|
||||||
""systemInstruction"": {{""parts"": [{{ ""text"": ""{persona}"" }}]}},
|
""systemInstruction"": {{""parts"": [{{ ""text"": ""{escapedPersona} {jsonInstruction}"" }}]}},
|
||||||
""contents"": [{{""parts"": [{{ ""text"": ""{prompt}"" }}]}}],
|
""contents"": [{{""parts"": [{{ ""text"": ""{escapedPrompt}"" }}]}}],
|
||||||
""generationConfig"": {{
|
""generationConfig"": {{
|
||||||
""maxOutputTokens"": 60,
|
""maxOutputTokens"": 100,
|
||||||
""temperature"": 0.7
|
""temperature"": 0.7,
|
||||||
|
""responseMimeType"": ""application/json""
|
||||||
}}
|
}}
|
||||||
}}";
|
}}";
|
||||||
|
|
||||||
var requestURL = $"{geminiURL}?key={apiKey}";
|
var requestURL = $"{geminiURL}?key={GetNextKey()}";
|
||||||
|
|
||||||
|
|
||||||
using (var request = new UnityWebRequest(requestURL, "POST"))
|
using (var request = new UnityWebRequest(requestURL, "POST"))
|
||||||
{
|
{
|
||||||
@@ -61,34 +99,25 @@ namespace Hallucinate.AI
|
|||||||
|
|
||||||
if (request.result == UnityWebRequest.Result.Success)
|
if (request.result == UnityWebRequest.Result.Success)
|
||||||
{
|
{
|
||||||
string rawResponse = request.downloadHandler.text;
|
var response = JsonUtility.FromJson<GeminiResponse>(request.downloadHandler.text);
|
||||||
try
|
if (response?.candidates?.Length > 0 && response.candidates[0].content?.parts?.Length > 0)
|
||||||
{
|
{
|
||||||
var response = JsonUtility.FromJson<GeminiResponse>(rawResponse);
|
onComplete?.Invoke(response.candidates[0].content.parts[0].text);
|
||||||
if (response != null &&
|
|
||||||
response.candidates != null &&
|
|
||||||
response.candidates.Length > 0 &&
|
|
||||||
response.candidates[0].content != null &&
|
|
||||||
response.candidates[0].content.parts != null &&
|
|
||||||
response.candidates[0].content.parts.Length > 0)
|
|
||||||
{
|
|
||||||
onComplete?.Invoke(response.candidates[0].content.parts[0].text);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Debug.LogWarning($"[Gemini] Response structure invalid or blocked. Raw: {rawResponse}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Debug.LogError($"[Gemini] JSON Parse Error: {e.Message}\nRaw Response: {rawResponse}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Debug.LogError($"[Gemini] API Error: {request.error}");
|
Debug.LogError($"[Gemini] API Error: {request.error}");
|
||||||
|
if (request.responseCode == 429)
|
||||||
|
{
|
||||||
|
nextRequestTime = Time.time + 60f; // Lock API for 1 minute
|
||||||
|
Debug.LogWarning("Quota Exceeded. API locked for 60s. Using fallback.");
|
||||||
|
}
|
||||||
|
onComplete?.Invoke(fallbackDialogues[UnityEngine.Random.Range(0, fallbackDialogues.Length)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
activeRequests--;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ public class GeminiTest : MonoBehaviour
|
|||||||
string testPrompt = "Chào bạn, nếu bạn nhận được tin nhắn này, hãy trả lời: 'Kết nối Gemini thành công!'";
|
string testPrompt = "Chào bạn, nếu bạn nhận được tin nhắn này, hãy trả lời: 'Kết nối Gemini thành công!'";
|
||||||
|
|
||||||
GeminiService.Instance.GetResponse(testPersona, testPrompt, (response) => {
|
GeminiService.Instance.GetResponse(testPersona, testPrompt, (response) => {
|
||||||
Debug.Log($"<color=green>[Gemini Test] Phản hồi từ API:</color> {response}");
|
string finalMsg = response;
|
||||||
|
try {
|
||||||
|
DialogueResult result = JsonUtility.FromJson<DialogueResult>(response);
|
||||||
|
finalMsg = result.text;
|
||||||
|
} catch { }
|
||||||
|
Debug.Log($"<color=green>[Gemini Test] Phản hồi từ API:</color> {finalMsg}");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,12 +67,17 @@ public class GerminiNPC : MonoBehaviour
|
|||||||
string prompt = $"Ta muốn bán cho ông món đồ này: {playerHeldItem}";
|
string prompt = $"Ta muốn bán cho ông món đồ này: {playerHeldItem}";
|
||||||
|
|
||||||
Hallucinate.AI.GeminiService.Instance.GetResponse(npcPersona, prompt, (response) => {
|
Hallucinate.AI.GeminiService.Instance.GetResponse(npcPersona, prompt, (response) => {
|
||||||
Debug.Log($"<color=green>Tom:</color> {response}");
|
string finalMsg = response;
|
||||||
|
try {
|
||||||
|
DialogueResult result = JsonUtility.FromJson<DialogueResult>(response);
|
||||||
|
finalMsg = result.text;
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
Debug.Log($"<color=green>Tom:</color> {finalMsg}");
|
||||||
AudioManager.Instance?.Play(responseSound, position: transform.position);
|
AudioManager.Instance?.Play(responseSound, position: transform.position);
|
||||||
|
|
||||||
// Nếu có ChatBubble gắn kèm thì hiển thị luôn
|
|
||||||
var bubble = GetComponentInChildren<Hallucinate.UI.ChatBubble>(true);
|
var bubble = GetComponentInChildren<Hallucinate.UI.ChatBubble>(true);
|
||||||
if (bubble != null) bubble.Show(response);
|
if (bubble != null) bubble.Show(finalMsg);
|
||||||
});
|
});
|
||||||
|
|
||||||
yield break;
|
yield break;
|
||||||
|
|||||||
32
Assets/Scripts/AI NPC/NoiseEmitter.cs
Normal file
32
Assets/Scripts/AI NPC/NoiseEmitter.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Hallucinate.AI
|
||||||
|
{
|
||||||
|
public class NoiseEmitter : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("Settings")]
|
||||||
|
public float defaultNoiseRange = 10f;
|
||||||
|
public LayerMask npcLayer;
|
||||||
|
|
||||||
|
public void EmitNoise(float volumeMultiplier = 1f)
|
||||||
|
{
|
||||||
|
float range = defaultNoiseRange * volumeMultiplier;
|
||||||
|
Collider[] hitColliders = Physics.OverlapSphere(transform.position, range, npcLayer);
|
||||||
|
|
||||||
|
foreach (var hit in hitColliders)
|
||||||
|
{
|
||||||
|
EnemyAI npc = hit.GetComponentInParent<EnemyAI>();
|
||||||
|
if (npc != null)
|
||||||
|
{
|
||||||
|
npc.HearNoise(transform.position, volumeMultiplier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDrawGizmosSelected()
|
||||||
|
{
|
||||||
|
Gizmos.color = new Color(1, 1, 0, 0.3f);
|
||||||
|
Gizmos.DrawWireSphere(transform.position, defaultNoiseRange);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
Assets/Scripts/AI NPC/NoiseEmitter.cs.meta
Normal file
2
Assets/Scripts/AI NPC/NoiseEmitter.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 67d06596d1741d34594e4a68adcaf257
|
||||||
@@ -38,6 +38,13 @@ namespace Invector
|
|||||||
EditorGUILayout.PropertyField(serializedObject.FindProperty("_useTriggerEnter"));
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("_useTriggerEnter"));
|
||||||
serializedObject.FindProperty("debugTextureName").boolValue = EditorGUILayout.Toggle("Debug Texture Name", serializedObject.FindProperty("debugTextureName").boolValue);
|
serializedObject.FindProperty("debugTextureName").boolValue = EditorGUILayout.Toggle("Debug Texture Name", serializedObject.FindProperty("debugTextureName").boolValue);
|
||||||
|
|
||||||
|
GUILayout.BeginVertical("box");
|
||||||
|
GUILayout.Box("AI Noise Settings", GUILayout.ExpandWidth(true));
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("emitAINoise"));
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("aiNoiseRange"));
|
||||||
|
EditorGUILayout.PropertyField(serializedObject.FindProperty("npcLayer"));
|
||||||
|
GUILayout.EndVertical();
|
||||||
|
|
||||||
if (serializedObject.FindProperty("animationType").enumValueIndex == (int)AnimationType.Humanoid)
|
if (serializedObject.FindProperty("animationType").enumValueIndex == (int)AnimationType.Humanoid)
|
||||||
{
|
{
|
||||||
GUILayout.BeginHorizontal("box");
|
GUILayout.BeginHorizontal("box");
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ namespace Invector
|
|||||||
public bool SpawnParticle { get { return _spawnParticle; } set { _spawnParticle = value; } }
|
public bool SpawnParticle { get { return _spawnParticle; } set { _spawnParticle = value; } }
|
||||||
public bool SpawnStepMark { get { return _spawnStepMark; } set { _spawnStepMark = value; } }
|
public bool SpawnStepMark { get { return _spawnStepMark; } set { _spawnStepMark = value; } }
|
||||||
|
|
||||||
|
[Header("AI Noise Settings")]
|
||||||
|
public bool emitAINoise = true;
|
||||||
|
public float aiNoiseRange = 10f;
|
||||||
|
public LayerMask npcLayer;
|
||||||
|
|
||||||
protected int surfaceIndex = 0;
|
protected int surfaceIndex = 0;
|
||||||
protected Terrain terrain;
|
protected Terrain terrain;
|
||||||
protected TerrainCollider terrainCollider;
|
protected TerrainCollider terrainCollider;
|
||||||
@@ -248,6 +253,37 @@ namespace Invector
|
|||||||
currentFootStep.spawnParticleEffect = SpawnParticle;
|
currentFootStep.spawnParticleEffect = SpawnParticle;
|
||||||
currentFootStep.spawnStepMarkEffect = SpawnStepMark;
|
currentFootStep.spawnStepMarkEffect = SpawnStepMark;
|
||||||
SpawnSurfaceEffect(currentFootStep);
|
SpawnSurfaceEffect(currentFootStep);
|
||||||
|
|
||||||
|
if (emitAINoise) EmitAINoise();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void EmitAINoise()
|
||||||
|
{
|
||||||
|
float currentRange = aiNoiseRange;
|
||||||
|
float currentVolume = Volume;
|
||||||
|
|
||||||
|
// Kiểm tra trạng thái ngồi từ Animator
|
||||||
|
Animator anim = GetComponent<Animator>();
|
||||||
|
if (anim != null)
|
||||||
|
{
|
||||||
|
// Nếu đang ngồi (IsCrouching = true), giảm 50% vùng phát hiện và âm lượng
|
||||||
|
if (anim.GetBool("IsCrouching"))
|
||||||
|
{
|
||||||
|
currentRange *= 0.5f;
|
||||||
|
currentVolume *= 0.5f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tìm tất cả Collider trong bán kính tiếng động thuộc Layer NPC
|
||||||
|
Collider[] hitColliders = Physics.OverlapSphere(transform.position, currentRange, npcLayer);
|
||||||
|
foreach (var hit in hitColliders)
|
||||||
|
{
|
||||||
|
var npc = hit.GetComponentInParent<EnemyAI>();
|
||||||
|
if (npc != null)
|
||||||
|
{
|
||||||
|
npc.HearNoise(transform.position, currentVolume);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ Material:
|
|||||||
- _ZWrite: 0
|
- _ZWrite: 0
|
||||||
m_Colors:
|
m_Colors:
|
||||||
- _CameraFadeParams: {r: 0, g: Infinity, b: 0, a: 0}
|
- _CameraFadeParams: {r: 0, g: Infinity, b: 0, a: 0}
|
||||||
- _Color: {r: 0.6132076, g: 0.1454958, b: 0.118592024, a: 0.1882353}
|
- _Color: {r: 1, g: 0.8260788, b: 0.08962262, a: 0.25}
|
||||||
- _Emission: {r: 0, g: 0, b: 0, a: 0}
|
- _Emission: {r: 0, g: 0, b: 0, a: 0}
|
||||||
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
- _EmissionColor: {r: 0, g: 0, b: 0, a: 1}
|
||||||
- _Flip: {r: 1, g: 1, b: 1, a: 1}
|
- _Flip: {r: 1, g: 1, b: 1, a: 1}
|
||||||
@@ -123,6 +123,6 @@ Material:
|
|||||||
- _SoftParticleFadeParams: {r: 0, g: 0, b: 0, a: 0}
|
- _SoftParticleFadeParams: {r: 0, g: 0, b: 0, a: 0}
|
||||||
- _SpecColor: {r: 0, g: 0, b: 0, a: 0}
|
- _SpecColor: {r: 0, g: 0, b: 0, a: 0}
|
||||||
- _Specular: {r: 1, g: 1, b: 1, a: 0}
|
- _Specular: {r: 1, g: 1, b: 1, a: 0}
|
||||||
- _TintColor: {r: 0.6132076, g: 0.1454958, b: 0.118592024, a: 0.1882353}
|
- _TintColor: {r: 1, g: 0.8260788, b: 0.08962262, a: 0.25}
|
||||||
m_BuildTextureStacks: []
|
m_BuildTextureStacks: []
|
||||||
m_AllowLocking: 1
|
m_AllowLocking: 1
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user