diff --git a/.idea/.idea.HALLUCINATE/.idea/workspace.xml b/.idea/.idea.HALLUCINATE/.idea/workspace.xml
index 118b9970..528b374f 100644
--- a/.idea/.idea.HALLUCINATE/.idea/workspace.xml
+++ b/.idea/.idea.HALLUCINATE/.idea/workspace.xml
@@ -5,27 +5,12 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
@@ -33,7 +18,7 @@
-
+
@@ -195,7 +180,7 @@
-
+
diff --git a/Assets/Prefabs/NPC/KamikazeAI.prefab b/Assets/Prefabs/NPC/KamikazeAI.prefab
new file mode 100644
index 00000000..e550b4b4
--- /dev/null
+++ b/Assets/Prefabs/NPC/KamikazeAI.prefab
@@ -0,0 +1,157 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &6425756872251228809
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 7442554217592711348}
+ - component: {fileID: 6259819009638257505}
+ - component: {fileID: 450573224913792705}
+ - component: {fileID: 6087599376744356948}
+ - component: {fileID: 3563399533700019190}
+ - component: {fileID: 681314853465352057}
+ m_Layer: 0
+ m_Name: KamikazeAI
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &7442554217592711348
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6425756872251228809}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 3.17522, y: 9.53715, z: -18.50002}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!33 &6259819009638257505
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6425756872251228809}
+ m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!23 &450573224913792705
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6425756872251228809}
+ m_Enabled: 1
+ m_CastShadows: 1
+ m_ReceiveShadows: 1
+ m_DynamicOccludee: 1
+ m_StaticShadowCaster: 0
+ m_MotionVectors: 1
+ m_LightProbeUsage: 1
+ m_ReflectionProbeUsage: 1
+ m_RayTracingMode: 2
+ m_RayTraceProcedural: 0
+ m_RayTracingAccelStructBuildFlagsOverride: 0
+ m_RayTracingAccelStructBuildFlags: 1
+ m_SmallMeshCulling: 1
+ m_ForceMeshLod: -1
+ m_MeshLodSelectionBias: 0
+ m_RenderingLayerMask: 1
+ m_RendererPriority: 0
+ m_Materials:
+ - {fileID: 2100000, guid: 9193f4635bbf98d46be9a6357461aa10, type: 2}
+ m_StaticBatchInfo:
+ firstSubMesh: 0
+ subMeshCount: 0
+ m_StaticBatchRoot: {fileID: 0}
+ m_ProbeAnchor: {fileID: 0}
+ m_LightProbeVolumeOverride: {fileID: 0}
+ m_ScaleInLightmap: 1
+ m_ReceiveGI: 1
+ m_PreserveUVs: 0
+ m_IgnoreNormalsForChartDetection: 0
+ m_ImportantGI: 0
+ m_StitchLightmapSeams: 1
+ m_SelectedEditorRenderState: 3
+ m_MinimumChartSize: 4
+ m_AutoUVMaxDistance: 0.5
+ m_AutoUVMaxAngle: 89
+ m_LightmapParameters: {fileID: 0}
+ m_GlobalIlluminationMeshLod: 0
+ m_SortingLayerID: 0
+ m_SortingLayer: 0
+ m_SortingOrder: 0
+ m_MaskInteraction: 0
+ m_AdditionalVertexStreams: {fileID: 0}
+--- !u!135 &6087599376744356948
+SphereCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6425756872251228809}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 0
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 3
+ m_Radius: 0.5
+ m_Center: {x: 0, y: 0, z: 0}
+--- !u!195 &3563399533700019190
+NavMeshAgent:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6425756872251228809}
+ m_Enabled: 1
+ m_AgentTypeID: 0
+ m_Radius: 0.5
+ m_Speed: 3.5
+ m_Acceleration: 8
+ avoidancePriority: 50
+ m_AngularSpeed: 120
+ m_StoppingDistance: 0
+ m_AutoTraverseOffMeshLink: 1
+ m_AutoBraking: 1
+ m_AutoRepath: 1
+ m_Height: 1
+ m_BaseOffset: 0.5
+ m_WalkableMask: 4294967295
+ m_ObstacleAvoidanceType: 4
+--- !u!114 &681314853465352057
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 6425756872251228809}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 6008ec58fb909034abd7293b55f0d558, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::KamikazeAI
+ player: {fileID: 0}
+ detectRange: 15
+ patrolSpeed: 2.5
+ chaseSpeed: 7
+ patrolRadius: 12
+ patrolWaitTime: 2
+ explosionEffectPrefab: {fileID: 8568474719719117872, guid: 39bf32dcd9299df4ca44fd10a817eda4, type: 3}
diff --git a/Assets/Prefabs/NPC/KamikazeAI.prefab.meta b/Assets/Prefabs/NPC/KamikazeAI.prefab.meta
new file mode 100644
index 00000000..ec62e0d6
--- /dev/null
+++ b/Assets/Prefabs/NPC/KamikazeAI.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 8947c5e2361e67945bda336253786233
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Prefabs/NPC/explosionEffectPrefab.prefab b/Assets/Prefabs/NPC/explosionEffectPrefab.prefab
new file mode 100644
index 00000000..109419e8
--- /dev/null
+++ b/Assets/Prefabs/NPC/explosionEffectPrefab.prefab
@@ -0,0 +1,105 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &8568474719719117872
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 9170216226365166400}
+ - component: {fileID: 5936645265828481346}
+ - component: {fileID: 372612951384622263}
+ - component: {fileID: 454324809550099347}
+ m_Layer: 0
+ m_Name: explosionEffectPrefab
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &9170216226365166400
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 8568474719719117872}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 3.21488, y: 9.58688, z: -18.00407}
+ m_LocalScale: {x: 3, y: 3, z: 3}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!33 &5936645265828481346
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 8568474719719117872}
+ m_Mesh: {fileID: 10207, guid: 0000000000000000e000000000000000, type: 0}
+--- !u!23 &372612951384622263
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 8568474719719117872}
+ m_Enabled: 1
+ m_CastShadows: 1
+ m_ReceiveShadows: 1
+ m_DynamicOccludee: 1
+ m_StaticShadowCaster: 0
+ m_MotionVectors: 1
+ m_LightProbeUsage: 1
+ m_ReflectionProbeUsage: 1
+ m_RayTracingMode: 2
+ m_RayTraceProcedural: 0
+ m_RayTracingAccelStructBuildFlagsOverride: 0
+ m_RayTracingAccelStructBuildFlags: 1
+ m_SmallMeshCulling: 1
+ m_ForceMeshLod: -1
+ m_MeshLodSelectionBias: 0
+ m_RenderingLayerMask: 1
+ m_RendererPriority: 0
+ m_Materials:
+ - {fileID: 2100000, guid: b0a84576fc378a24cbb3bfc7be45a02e, type: 2}
+ m_StaticBatchInfo:
+ firstSubMesh: 0
+ subMeshCount: 0
+ m_StaticBatchRoot: {fileID: 0}
+ m_ProbeAnchor: {fileID: 0}
+ m_LightProbeVolumeOverride: {fileID: 0}
+ m_ScaleInLightmap: 1
+ m_ReceiveGI: 1
+ m_PreserveUVs: 0
+ m_IgnoreNormalsForChartDetection: 0
+ m_ImportantGI: 0
+ m_StitchLightmapSeams: 1
+ m_SelectedEditorRenderState: 3
+ m_MinimumChartSize: 4
+ m_AutoUVMaxDistance: 0.5
+ m_AutoUVMaxAngle: 89
+ m_LightmapParameters: {fileID: 0}
+ m_GlobalIlluminationMeshLod: 0
+ m_SortingLayerID: 0
+ m_SortingLayer: 0
+ m_SortingOrder: 0
+ m_MaskInteraction: 0
+ m_AdditionalVertexStreams: {fileID: 0}
+--- !u!114 &454324809550099347
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 8568474719719117872}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 950ee3c6c086a3b4fa9a7f1e544c1651, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Assembly-CSharp::AutoDestroy
diff --git a/Assets/Prefabs/NPC/explosionEffectPrefab.prefab.meta b/Assets/Prefabs/NPC/explosionEffectPrefab.prefab.meta
new file mode 100644
index 00000000..f76bcc34
--- /dev/null
+++ b/Assets/Prefabs/NPC/explosionEffectPrefab.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: 39bf32dcd9299df4ca44fd10a817eda4
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/AI NPC/AutoDestroy.cs b/Assets/Scripts/AI NPC/AutoDestroy.cs
new file mode 100644
index 00000000..fa9d1645
--- /dev/null
+++ b/Assets/Scripts/AI NPC/AutoDestroy.cs
@@ -0,0 +1,16 @@
+using UnityEngine;
+
+public class AutoDestroy : MonoBehaviour
+{
+ // Start is called once before the first execution of Update after the MonoBehaviour is created
+ void Start()
+ {
+
+ }
+
+ // Update is called once per frame
+ void Update()
+ {
+
+ }
+}
diff --git a/Assets/Scripts/AI NPC/AutoDestroy.cs.meta b/Assets/Scripts/AI NPC/AutoDestroy.cs.meta
new file mode 100644
index 00000000..1117075a
--- /dev/null
+++ b/Assets/Scripts/AI NPC/AutoDestroy.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 950ee3c6c086a3b4fa9a7f1e544c1651
\ No newline at end of file
diff --git a/Assets/Scripts/AI NPC/ChatBubble.cs b/Assets/Scripts/AI NPC/ChatBubble.cs
new file mode 100644
index 00000000..875796c1
--- /dev/null
+++ b/Assets/Scripts/AI NPC/ChatBubble.cs
@@ -0,0 +1,42 @@
+using UnityEngine;
+using TMPro;
+using PrimeTween;
+
+namespace Hallucinate.UI
+{
+ public class ChatBubble : MonoBehaviour
+ {
+ [SerializeField] private TextMeshProUGUI textDisplay;
+ [SerializeField] private CanvasGroup canvasGroup;
+ [SerializeField] private RectTransform bubbleRect;
+
+ private Transform mainCameraTransform;
+
+ private void Awake()
+ {
+ mainCameraTransform = Camera.main.transform;
+ canvasGroup.alpha = 0;
+ gameObject.SetActive(false);
+ }
+
+ private void LateUpdate()
+ {
+ // Billboard effect
+ transform.LookAt(transform.position + mainCameraTransform.rotation * Vector3.forward, mainCameraTransform.rotation * Vector3.up);
+ }
+
+ public void Show(string text, float duration = 4f)
+ {
+ gameObject.SetActive(true);
+ textDisplay.text = text;
+
+ // Animation using PrimeTween
+ PrimeTween.Sequence.Create()
+ .Group(Tween.Alpha(canvasGroup, 1f, 0.3f))
+ .Group(Tween.Scale(bubbleRect, Vector3.zero, Vector3.one, 0.4f, Ease.OutBack))
+ .Chain(Tween.Delay(duration))
+ .Chain(Tween.Alpha(canvasGroup, 0f, 0.5f))
+ .OnComplete(() => gameObject.SetActive(false));
+ }
+ }
+}
diff --git a/Assets/Scripts/AI NPC/ChatBubble.cs.meta b/Assets/Scripts/AI NPC/ChatBubble.cs.meta
new file mode 100644
index 00000000..b4482576
--- /dev/null
+++ b/Assets/Scripts/AI NPC/ChatBubble.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: ea510cea4b9ed1547ae4725a2ded949a
\ No newline at end of file
diff --git a/Assets/Scripts/AI NPC/GeminiService.cs b/Assets/Scripts/AI NPC/GeminiService.cs
new file mode 100644
index 00000000..98754b51
--- /dev/null
+++ b/Assets/Scripts/AI NPC/GeminiService.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Collections;
+using System.Text;
+using UnityEngine;
+using UnityEngine.Networking;
+
+namespace Hallucinate.AI
+{
+ [Serializable]
+ public class Part { public string text; }
+
+ [Serializable]
+ public class Content { public Part[] parts; }
+
+ [Serializable]
+ public class Candidate { public Content content; }
+
+ [Serializable]
+ public class GeminiResponse { public Candidate[] candidates; }
+
+ public class GeminiService : MonoBehaviour
+ {
+ public static GeminiService Instance { get; private set; }
+
+ [SerializeField] private string apiKey = "AQ.Ab8RN6I2hU_p8yHiPNNHtWzYBiLugbPP22gC6lzTWaYEWj4v0g"; // Replace with your key
+ [SerializeField] private string geminiURL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-latest:generateContent";
+
+ private void Awake()
+ {
+ if (Instance == null) { Instance = this; DontDestroyOnLoad(gameObject); }
+ else { Destroy(gameObject); }
+ }
+
+ public void GetResponse(string persona, string prompt, Action onComplete)
+ {
+ StartCoroutine(PostRequest(persona, prompt, onComplete));
+ }
+
+ private IEnumerator PostRequest(string persona, string prompt, Action onComplete)
+ {
+ var jsonBody = $@"{{
+ ""systemInstruction"": {{""parts"": [{{ ""text"": ""{persona}"" }}]}},
+ ""contents"": [{{""parts"": [{{ ""text"": ""{prompt}"" }}]}}]
+ }}";
+
+ var requestURL = $"{geminiURL}?key={apiKey}";
+
+ using (var request = new UnityWebRequest(requestURL, "POST"))
+ {
+ byte[] bodyRaw = Encoding.UTF8.GetBytes(jsonBody);
+ request.uploadHandler = new UploadHandlerRaw(bodyRaw);
+ request.downloadHandler = new DownloadHandlerBuffer();
+ request.SetRequestHeader("Content-Type", "application/json");
+
+ yield return request.SendWebRequest();
+
+ if (request.result == UnityWebRequest.Result.Success)
+ {
+ try
+ {
+ var response = JsonUtility.FromJson(request.downloadHandler.text);
+ if (response?.candidates != null && response.candidates.Length > 0)
+ {
+ onComplete?.Invoke(response.candidates[0].content.parts[0].text);
+ }
+ }
+ catch (Exception e) { Debug.LogError($"[Gemini] JSON Parse Error: {e.Message}"); }
+ }
+ else
+ {
+ Debug.LogError($"[Gemini] API Error: {request.error}");
+ }
+ }
+ }
+ }
+}
diff --git a/Assets/Scripts/AI NPC/GeminiService.cs.meta b/Assets/Scripts/AI NPC/GeminiService.cs.meta
new file mode 100644
index 00000000..6eb0ecb1
--- /dev/null
+++ b/Assets/Scripts/AI NPC/GeminiService.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: a859fc8e9ec10a347a3704b6045ca7e8
\ No newline at end of file
diff --git a/Assets/Scripts/AI NPC/GerminiNPC.cs b/Assets/Scripts/AI NPC/GerminiNPC.cs
index fc9443c7..8b2d776b 100644
--- a/Assets/Scripts/AI NPC/GerminiNPC.cs
+++ b/Assets/Scripts/AI NPC/GerminiNPC.cs
@@ -1,44 +1,13 @@
-using System;
-using System.Collections;
-using System.Text;
using UnityEngine;
using UnityEngine.InputSystem;
-using UnityEngine.Networking;
+using System.Collections;
using Hallucinate.Audio;
-
-[Serializable]
-public class Part
-{
- public string text;
-}
-
-[Serializable]
-public class Content
-{
- public Part[] parts;
-}
-
-[Serializable]
-public class Candidate
-{
- public Content content;
-}
-
-[Serializable]
-public class GeminiResponse
-{
- public Candidate[] candidates;
-}
+using Hallucinate.AI;
public class GerminiNPC : MonoBehaviour
{
[SerializeField]
- private string apiKey = "AQ.Ab8RN6I2hU_p8yHiPNNHtWzYBiLugbPP22gC6lzTWaYEWj4v0g";
- [SerializeField]
- private string germiniURL =
- "https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-latest:generateContent";
-
- public string npcPersona =
+ private string npcPersona =
"Ngươi là một lão thợ rèn cọc cằn tên là Tom, ngươi rất ghét những kẻ mang phế liệu đến tiệm của mình. Chỉ trả lời ngắn gọn trong 2 câu, theo phong cách trung cổ.";
public string playerHeldItem = "Thanh kiếm rỉ sét";
@@ -95,46 +64,17 @@ public class GerminiNPC : MonoBehaviour
private IEnumerator GetGerminiReponse()
{
- var jsonBody = $@"{{
- ""systemInstruction"": {{""parts"": [{{ ""text"": ""{npcPersona}"" }}]}},
- ""contents"": [{{""parts"": [{{ ""text"": ""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) => {
+ Debug.Log($"Tom: {response}");
+ AudioManager.Instance?.Play(responseSound, position: transform.position);
+
+ // Nếu có ChatBubble gắn kèm thì hiển thị luôn
+ var bubble = GetComponentInChildren(true);
+ if (bubble != null) bubble.Show(response);
+ });
- // 1. Sửa tham số thành ?key= (trước đó là ?ket=)
- var requestURL = $"{germiniURL}?key={apiKey}";
-
- // 2. Sử dụng requestURL (có chứa key) thay vì germiniURL gốc
- using (var request = new UnityWebRequest(requestURL, "POST"))
- {
- var bodyRaw = Encoding.UTF8.GetBytes(jsonBody);
- request.uploadHandler = new UploadHandlerRaw(bodyRaw);
- request.downloadHandler = new DownloadHandlerBuffer();
- request.SetRequestHeader("Content-Type", "application/json");
-
- yield return request.SendWebRequest();
-
- if (request.result == UnityWebRequest.Result.ProtocolError || request.result == UnityWebRequest.Result.ConnectionError)
- {
- Debug.LogError($"[Gemini Error] {request.error} - Response: {request.downloadHandler.text}");
- }
- else
- {
- var responseTEXT = request.downloadHandler.text;
- try
- {
- var geminiResponse = JsonUtility.FromJson(responseTEXT);
- if (geminiResponse != null && geminiResponse.candidates != null && geminiResponse.candidates.Length > 0)
- {
- var npcResponse = geminiResponse.candidates[0].content.parts[0].text;
- Debug.Log($"Tom: {npcResponse}");
- AudioManager.Instance?.Play(responseSound, position: transform.position);
- }
- }
- catch (Exception e)
- {
- Debug.LogError($"[JSON Parse Error] {e.Message}");
- }
- }
- }
+ yield break;
}
}
diff --git a/Assets/Scripts/AI NPC/KamikazeAI.cs b/Assets/Scripts/AI NPC/KamikazeAI.cs
new file mode 100644
index 00000000..ea69d67f
--- /dev/null
+++ b/Assets/Scripts/AI NPC/KamikazeAI.cs
@@ -0,0 +1,198 @@
+using System.Collections;
+using System.Collections.Generic;
+using UnityEngine;
+using UnityEngine.AI;
+
+[RequireComponent(typeof(NavMeshAgent))]
+public class KamikazeAI : MonoBehaviour
+{
+ [Header("References")]
+ public Transform player;
+
+ [Header("Detection")]
+ public float detectRange = 15f;
+ private bool canSeePlayer = false;
+
+ [Header("Movement & Random Patrol")]
+ public float patrolSpeed = 2.5f;
+ public float chaseSpeed = 7f;
+ public float patrolRadius = 12f; // Bán kính của khu vực tuần tra ngẫu nhiên
+ public float patrolWaitTime = 2f; // Thời gian đứng nghỉ trước khi đổi sang điểm ngẫu nhiên mới
+
+ private Vector3 startPosition; // Tâm của khu vực tuần tra (Vị trí ban đầu)
+ private float currentWaitTime;
+ private NavMeshAgent agent;
+ private bool isExploding = false;
+ public Node behaviorTreeRoot;
+ public GameObject explosionEffectPrefab;
+
+ private void Start()
+ {
+ agent = GetComponent();
+
+ // Lưu lại vị trí xuất phát để làm tâm, NPC sẽ chỉ đi loay hoay quanh khu vực này
+ startPosition = transform.position;
+
+ InitBehaviorTree();
+ }
+
+ private void Update()
+ {
+ if (isExploding) return;
+
+ if (player == null) FindPlayer();
+ else CheckVision();
+
+ behaviorTreeRoot?.Evaluate();
+ }
+
+ private void FindPlayer()
+ {
+ GameObject playerObj = GameObject.FindGameObjectWithTag("Player");
+ if (playerObj != null) player = playerObj.transform;
+ }
+
+ private void CheckVision()
+ {
+ if (Vector3.Distance(transform.position, player.position) <= detectRange)
+ canSeePlayer = true;
+ else
+ canSeePlayer = false;
+ }
+
+ private void InitBehaviorTree()
+ {
+ var explodeSequence = new Sequence(new List
+ {
+ new TaskNode(CheckIsCloseEnoughToExplode),
+ new TaskNode(ActionTriggerExplosion)
+ });
+
+ var chaseSequence = new Sequence(new List
+ {
+ new TaskNode(CheckCanSeePlayer),
+ new TaskNode(ActionChase)
+ });
+
+ // Hành động tuần tra ngẫu nhiên
+ var patrolNode = new TaskNode(ActionRandomPatrol);
+
+ behaviorTreeRoot = new Selector(new List
+ {
+ explodeSequence,
+ chaseSequence,
+ patrolNode
+ });
+ }
+
+ #region CONDITIONS
+
+ private NodeState CheckCanSeePlayer()
+ {
+ return canSeePlayer ? NodeState.Success : NodeState.Failure;
+ }
+
+ private NodeState CheckIsCloseEnoughToExplode()
+ {
+ if (player == null) return NodeState.Failure;
+ float dist = Vector3.Distance(transform.position, player.position);
+ return dist <= 3f ? NodeState.Success : NodeState.Failure;
+ }
+
+ #endregion
+
+ #region ACTIONS
+
+ // HÀM TUẦN TRA NGẪU NHIÊN MỚI
+ private NodeState ActionRandomPatrol()
+ {
+ Debug.Log("Wandering randomly...");
+ agent.isStopped = false;
+ agent.speed = patrolSpeed;
+
+ // 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;
+
+ // Đứng đợi hết thời gian quy định rồi mới tìm đường mới
+ if (currentWaitTime >= patrolWaitTime)
+ {
+ // 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ờ
+ }
+ }
+
+ return NodeState.Running;
+ }
+
+ private NodeState ActionChase()
+ {
+ if (player == null) return NodeState.Failure;
+
+ Debug.Log("Kamikaze is rushing you!");
+ agent.isStopped = false;
+ agent.speed = chaseSpeed;
+ agent.SetDestination(player.position);
+
+ return NodeState.Running;
+ }
+
+ private NodeState ActionTriggerExplosion()
+ {
+ StartCoroutine(ExplosionRoutine());
+ return NodeState.Success;
+ }
+
+ #endregion
+
+ #region EXPLOSION LOGIC
+
+ private IEnumerator ExplosionRoutine()
+ {
+ isExploding = true;
+ agent.isStopped = true;
+ agent.velocity = Vector3.zero;
+
+ Debug.Log("BOMB ARMED!");
+ yield return new WaitForSeconds(1.5f);
+
+ if (player != null)
+ {
+ float distToPlayer = Vector3.Distance(transform.position, player.position);
+ if (distToPlayer <= 4f)
+ {
+ Debug.Log("BOOM! Player took damage!");
+ }
+ }
+
+ if (explosionEffectPrefab != null)
+ {
+ Instantiate(explosionEffectPrefab, transform.position, Quaternion.identity);
+ }
+
+ Destroy(gameObject);
+ }
+
+ #endregion
+
+ // Vẽ vùng giới hạn tuần tra màu xanh lá cây trên Scene để bạn dễ căn chỉnh độ rộng
+ private void OnDrawGizmosSelected()
+ {
+ Gizmos.color = Color.green;
+ // Nếu game đang chạy thì vẽ quanh tâm startPosition, nếu chưa chạy thì vẽ quanh vị trí hiện tại
+ Vector3 center = Application.isPlaying ? startPosition : transform.position;
+ Gizmos.DrawWireSphere(center, patrolRadius);
+ }
+}
\ No newline at end of file
diff --git a/Assets/Scripts/AI NPC/KamikazeAI.cs.meta b/Assets/Scripts/AI NPC/KamikazeAI.cs.meta
new file mode 100644
index 00000000..f45ea964
--- /dev/null
+++ b/Assets/Scripts/AI NPC/KamikazeAI.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 6008ec58fb909034abd7293b55f0d558
\ No newline at end of file