Update
This commit is contained in:
BIN
.gemini-workspace-history/session-2026-06-06-00-13.json.gz
Normal file
BIN
.gemini-workspace-history/session-2026-06-06-00-13.json.gz
Normal file
Binary file not shown.
5
.idea/.idea.HALLUCINATE/.idea/workspace.xml
generated
5
.idea/.idea.HALLUCINATE/.idea/workspace.xml
generated
@@ -6,11 +6,8 @@
|
|||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="f9183c68-daf0-43b8-be4c-fad79983f91b" name="Changes" comment="">
|
<list default="true" id="f9183c68-daf0-43b8-be4c-fad79983f91b" name="Changes" comment="">
|
||||||
<change beforePath="$PROJECT_DIR$/.idea/.idea.HALLUCINATE/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.HALLUCINATE/.idea/workspace.xml" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/.idea/.idea.HALLUCINATE/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/.idea.HALLUCINATE/.idea/workspace.xml" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/Assets/Prefabs/NPC/KamikazeAI.prefab" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Prefabs/NPC/KamikazeAI.prefab" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/Assets/Prefabs/NPC/xNPC.prefab" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Prefabs/NPC/xNPC.prefab" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/Assets/Scenes/Cho môn AI/Only AI.unity" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scenes/Cho môn AI/Only AI.unity" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/Assets/Scenes/Cho môn AI/Only AI.unity" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scenes/Cho môn AI/Only AI.unity" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/Assets/Scripts/AI NPC/EnemyAI.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/AI NPC/EnemyAI.cs" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/Assets/Scripts/AI NPC/EnemyAI.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/AI NPC/EnemyAI.cs" afterDir="false" />
|
||||||
<change beforePath="$PROJECT_DIR$/Assets/Scripts/Player/Weapon/vShooterWeaponBase.cs" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Scripts/Player/Weapon/vShooterWeaponBase.cs" afterDir="false" />
|
|
||||||
<change beforePath="$PROJECT_DIR$/Assets/Third Parties/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Fallback.asset" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Third Parties/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Fallback.asset" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/Assets/Third Parties/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Fallback.asset" beforeDir="false" afterPath="$PROJECT_DIR$/Assets/Third Parties/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Fallback.asset" afterDir="false" />
|
||||||
</list>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
@@ -181,7 +178,7 @@
|
|||||||
<workItem from="1780364354282" duration="4357000" />
|
<workItem from="1780364354282" duration="4357000" />
|
||||||
<workItem from="1780409218377" duration="9852000" />
|
<workItem from="1780409218377" duration="9852000" />
|
||||||
<workItem from="1780494322686" duration="643000" />
|
<workItem from="1780494322686" duration="643000" />
|
||||||
<workItem from="1780633654231" duration="28766000" />
|
<workItem from="1780633654231" duration="29762000" />
|
||||||
</task>
|
</task>
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ GameObject:
|
|||||||
m_Icon: {fileID: 0}
|
m_Icon: {fileID: 0}
|
||||||
m_NavMeshLayer: 0
|
m_NavMeshLayer: 0
|
||||||
m_StaticEditorFlags: 0
|
m_StaticEditorFlags: 0
|
||||||
m_IsActive: 0
|
m_IsActive: 1
|
||||||
--- !u!4 &5863061020199015852
|
--- !u!4 &5863061020199015852
|
||||||
Transform:
|
Transform:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -235,7 +235,7 @@ Transform:
|
|||||||
m_GameObject: {fileID: 5687887011233860168}
|
m_GameObject: {fileID: 5687887011233860168}
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
|
||||||
m_LocalPosition: {x: 0.041, y: 1.5, z: -0.007}
|
m_LocalPosition: {x: 0.002, y: 1.5, z: 0.514}
|
||||||
m_LocalScale: {x: 0.05, y: 0.05, z: 0.05}
|
m_LocalScale: {x: 0.05, y: 0.05, z: 0.05}
|
||||||
m_ConstrainProportionsScale: 1
|
m_ConstrainProportionsScale: 1
|
||||||
m_Children: []
|
m_Children: []
|
||||||
@@ -525,6 +525,11 @@ MonoBehaviour:
|
|||||||
dodgeForce: 8
|
dodgeForce: 8
|
||||||
dodgeDuration: 0.5
|
dodgeDuration: 0.5
|
||||||
dodgeCooldown: 3
|
dodgeCooldown: 3
|
||||||
|
isPanicking: 0
|
||||||
|
isEnraged: 0
|
||||||
|
panicHealthThreshold: 40
|
||||||
|
regenRate: 2
|
||||||
|
regenDelay: 5
|
||||||
minStrafeDuration: 0.5
|
minStrafeDuration: 0.5
|
||||||
maxStrafeDuration: 2.2
|
maxStrafeDuration: 2.2
|
||||||
maxSpreadAngle: 3
|
maxSpreadAngle: 3
|
||||||
@@ -641,14 +646,14 @@ CapsuleCollider:
|
|||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Bits: 0
|
m_Bits: 0
|
||||||
m_LayerOverridePriority: 0
|
m_LayerOverridePriority: 0
|
||||||
m_IsTrigger: 1
|
m_IsTrigger: 0
|
||||||
m_ProvidesContacts: 0
|
m_ProvidesContacts: 0
|
||||||
m_Enabled: 1
|
m_Enabled: 1
|
||||||
serializedVersion: 2
|
serializedVersion: 2
|
||||||
m_Radius: 0.37279892
|
m_Radius: 0.41056824
|
||||||
m_Height: 1.8474874
|
m_Height: 1.8474874
|
||||||
m_Direction: 1
|
m_Direction: 1
|
||||||
m_Center: {x: 0, y: 0.92911196, z: -0.24853802}
|
m_Center: {x: 0, y: 0.92911196, z: 0.061820984}
|
||||||
--- !u!114 &204793640880232070
|
--- !u!114 &204793640880232070
|
||||||
MonoBehaviour:
|
MonoBehaviour:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
|
|||||||
@@ -246,8 +246,8 @@ MonoBehaviour:
|
|||||||
m_Script: {fileID: 11500000, guid: 4e4f602386d4d484ea7a2a3b0c19ac21, type: 3}
|
m_Script: {fileID: 11500000, guid: 4e4f602386d4d484ea7a2a3b0c19ac21, type: 3}
|
||||||
m_Name:
|
m_Name:
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::LaserProjectile
|
m_EditorClassIdentifier: Assembly-CSharp::LaserProjectile
|
||||||
speed: 100
|
speed: 70
|
||||||
lifeTime: 5
|
lifeTime: 2
|
||||||
damageAmount: 10
|
damageAmount: 10
|
||||||
hitSound: Laser_Hit
|
hitSound: Laser_Hit
|
||||||
--- !u!54 &-8021594009672363794
|
--- !u!54 &-8021594009672363794
|
||||||
|
|||||||
@@ -127,6 +127,14 @@ PrefabInstance:
|
|||||||
serializedVersion: 3
|
serializedVersion: 3
|
||||||
m_TransformParent: {fileID: 1730236067}
|
m_TransformParent: {fileID: 1730236067}
|
||||||
m_Modifications:
|
m_Modifications:
|
||||||
|
- target: {fileID: 681314853465352057, guid: 8947c5e2361e67945bda336253786233, type: 3}
|
||||||
|
propertyPath: player
|
||||||
|
value:
|
||||||
|
objectReference: {fileID: 1042553198}
|
||||||
|
- target: {fileID: 681314853465352057, guid: 8947c5e2361e67945bda336253786233, type: 3}
|
||||||
|
propertyPath: m_Enabled
|
||||||
|
value: 1
|
||||||
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 6425756872251228809, guid: 8947c5e2361e67945bda336253786233, type: 3}
|
- target: {fileID: 6425756872251228809, guid: 8947c5e2361e67945bda336253786233, type: 3}
|
||||||
propertyPath: m_Name
|
propertyPath: m_Name
|
||||||
value: KamikazeAI
|
value: KamikazeAI
|
||||||
@@ -90677,7 +90685,7 @@ PrefabInstance:
|
|||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 7522161431095319480, guid: 15df559ce497e104a81254e0adf3107e, type: 3}
|
- target: {fileID: 7522161431095319480, guid: 15df559ce497e104a81254e0adf3107e, type: 3}
|
||||||
propertyPath: m_IsActive
|
propertyPath: m_IsActive
|
||||||
value: 1
|
value: 0
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 7561534673732080652, guid: 15df559ce497e104a81254e0adf3107e, type: 3}
|
- target: {fileID: 7561534673732080652, guid: 15df559ce497e104a81254e0adf3107e, type: 3}
|
||||||
propertyPath: m_LocalPosition.x
|
propertyPath: m_LocalPosition.x
|
||||||
@@ -93268,13 +93276,14 @@ PrefabInstance:
|
|||||||
propertyPath: m_AnchorMax.y
|
propertyPath: m_AnchorMax.y
|
||||||
value: 0
|
value: 0
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
|
- target: {fileID: 6688537537024804213, guid: 092f8aa667b2004459042a82a4c9e41d, type: 3}
|
||||||
|
propertyPath: openCloseWindow
|
||||||
|
value: 1
|
||||||
|
objectReference: {fileID: 0}
|
||||||
m_RemovedComponents: []
|
m_RemovedComponents: []
|
||||||
m_RemovedGameObjects: []
|
m_RemovedGameObjects: []
|
||||||
m_AddedGameObjects: []
|
m_AddedGameObjects: []
|
||||||
m_AddedComponents:
|
m_AddedComponents: []
|
||||||
- targetCorrespondingSourceObject: {fileID: 146720, guid: 092f8aa667b2004459042a82a4c9e41d, type: 3}
|
|
||||||
insertIndex: -1
|
|
||||||
addedObject: {fileID: 1042553199}
|
|
||||||
m_SourcePrefab: {fileID: 100100000, guid: 092f8aa667b2004459042a82a4c9e41d, type: 3}
|
m_SourcePrefab: {fileID: 100100000, guid: 092f8aa667b2004459042a82a4c9e41d, type: 3}
|
||||||
--- !u!1 &649955262
|
--- !u!1 &649955262
|
||||||
GameObject:
|
GameObject:
|
||||||
@@ -94195,7 +94204,7 @@ PrefabInstance:
|
|||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 7522161431095319480, guid: 15df559ce497e104a81254e0adf3107e, type: 3}
|
- target: {fileID: 7522161431095319480, guid: 15df559ce497e104a81254e0adf3107e, type: 3}
|
||||||
propertyPath: m_IsActive
|
propertyPath: m_IsActive
|
||||||
value: 1
|
value: 0
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 7561534673732372480, guid: 15df559ce497e104a81254e0adf3107e, type: 3}
|
- target: {fileID: 7561534673732372480, guid: 15df559ce497e104a81254e0adf3107e, type: 3}
|
||||||
propertyPath: m_Layer
|
propertyPath: m_Layer
|
||||||
@@ -108613,7 +108622,7 @@ PrefabInstance:
|
|||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 7522161431095319480, guid: 15df559ce497e104a81254e0adf3107e, type: 3}
|
- target: {fileID: 7522161431095319480, guid: 15df559ce497e104a81254e0adf3107e, type: 3}
|
||||||
propertyPath: m_IsActive
|
propertyPath: m_IsActive
|
||||||
value: 1
|
value: 0
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 7561534673732372480, guid: 15df559ce497e104a81254e0adf3107e, type: 3}
|
- target: {fileID: 7561534673732372480, guid: 15df559ce497e104a81254e0adf3107e, type: 3}
|
||||||
propertyPath: m_Layer
|
propertyPath: m_Layer
|
||||||
@@ -109251,32 +109260,11 @@ Mesh:
|
|||||||
- serializedVersion: 1
|
- serializedVersion: 1
|
||||||
m_IndexStart: 0
|
m_IndexStart: 0
|
||||||
m_IndexCount: 0
|
m_IndexCount: 0
|
||||||
--- !u!1 &1042553173 stripped
|
|
||||||
GameObject:
|
|
||||||
m_CorrespondingSourceObject: {fileID: 146720, guid: 092f8aa667b2004459042a82a4c9e41d, type: 3}
|
|
||||||
m_PrefabInstance: {fileID: 647991887}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
--- !u!4 &1042553198 stripped
|
--- !u!4 &1042553198 stripped
|
||||||
Transform:
|
Transform:
|
||||||
m_CorrespondingSourceObject: {fileID: 404034, guid: 092f8aa667b2004459042a82a4c9e41d, type: 3}
|
m_CorrespondingSourceObject: {fileID: 404034, guid: 092f8aa667b2004459042a82a4c9e41d, type: 3}
|
||||||
m_PrefabInstance: {fileID: 647991887}
|
m_PrefabInstance: {fileID: 647991887}
|
||||||
m_PrefabAsset: {fileID: 0}
|
m_PrefabAsset: {fileID: 0}
|
||||||
--- !u!114 &1042553199
|
|
||||||
MonoBehaviour:
|
|
||||||
m_ObjectHideFlags: 0
|
|
||||||
m_CorrespondingSourceObject: {fileID: 0}
|
|
||||||
m_PrefabInstance: {fileID: 0}
|
|
||||||
m_PrefabAsset: {fileID: 0}
|
|
||||||
m_GameObject: {fileID: 1042553173}
|
|
||||||
m_Enabled: 1
|
|
||||||
m_EditorHideFlags: 0
|
|
||||||
m_Script: {fileID: 11500000, guid: 67d06596d1741d34594e4a68adcaf257, type: 3}
|
|
||||||
m_Name:
|
|
||||||
m_EditorClassIdentifier: Assembly-CSharp::Hallucinate.AI.NoiseEmitter
|
|
||||||
defaultNoiseRange: 10
|
|
||||||
npcLayer:
|
|
||||||
serializedVersion: 2
|
|
||||||
m_Bits: 512
|
|
||||||
--- !u!1 &1051392855
|
--- !u!1 &1051392855
|
||||||
GameObject:
|
GameObject:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -142075,6 +142063,7 @@ MonoBehaviour:
|
|||||||
winSound: UI_Win
|
winSound: UI_Win
|
||||||
warningSound: UI_Warning
|
warningSound: UI_Warning
|
||||||
clickSound: UI_Click
|
clickSound: UI_Click
|
||||||
|
playerTag: Player
|
||||||
--- !u!65 &1398230981
|
--- !u!65 &1398230981
|
||||||
BoxCollider:
|
BoxCollider:
|
||||||
m_ObjectHideFlags: 0
|
m_ObjectHideFlags: 0
|
||||||
@@ -151732,7 +151721,7 @@ PrefabInstance:
|
|||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 7522161431095319480, guid: 15df559ce497e104a81254e0adf3107e, type: 3}
|
- target: {fileID: 7522161431095319480, guid: 15df559ce497e104a81254e0adf3107e, type: 3}
|
||||||
propertyPath: m_IsActive
|
propertyPath: m_IsActive
|
||||||
value: 1
|
value: 0
|
||||||
objectReference: {fileID: 0}
|
objectReference: {fileID: 0}
|
||||||
- target: {fileID: 7561534673732372480, guid: 15df559ce497e104a81254e0adf3107e, type: 3}
|
- target: {fileID: 7561534673732372480, guid: 15df559ce497e104a81254e0adf3107e, type: 3}
|
||||||
propertyPath: m_Layer
|
propertyPath: m_Layer
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
using System;
|
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 UnityEngine.InputSystem;
|
||||||
|
using Invector;
|
||||||
|
using Invector.vCharacterController;
|
||||||
using Random = UnityEngine.Random;
|
using Random = UnityEngine.Random;
|
||||||
|
|
||||||
[Serializable]
|
[Serializable]
|
||||||
public class DialogueResult { public string text; public float speedMod; public float suspicionMod; }
|
public class DialogueResult { public string text; public float speedMod; public float suspicionMod; }
|
||||||
|
|
||||||
// Quy trình ưu tiên: Né đòn --> Bắn hạ (Artifact / Sound Aggro) --> Đuổi theo (Vector) --> Điều tra --> Nói chuyện --> Đi tuần
|
// Quy trình ưu tiên: Né đòn (Bị bắn) --> Panic (Thấp máu) --> Bắn hạ (Chiến đấu) --> Đuổi theo --> Điều tra --> Nói chuyện --> Đi tuần
|
||||||
[RequireComponent(typeof(NavMeshAgent))]
|
[RequireComponent(typeof(NavMeshAgent))]
|
||||||
[RequireComponent(typeof(Rigidbody))]
|
[RequireComponent(typeof(Rigidbody))]
|
||||||
|
[RequireComponent(typeof(vHealthController))]
|
||||||
public class EnemyAI : MonoBehaviour
|
public class EnemyAI : MonoBehaviour
|
||||||
{
|
{
|
||||||
[Header("References")]
|
[Header("References")]
|
||||||
@@ -21,6 +24,7 @@ public class EnemyAI : MonoBehaviour
|
|||||||
private Rigidbody rb;
|
private Rigidbody rb;
|
||||||
private FieldOfView fov;
|
private FieldOfView fov;
|
||||||
private Collider mainCollider;
|
private Collider mainCollider;
|
||||||
|
private vHealthController health;
|
||||||
|
|
||||||
[Header("Movement Settings")]
|
[Header("Movement Settings")]
|
||||||
public float moveSpeed = 3f;
|
public float moveSpeed = 3f;
|
||||||
@@ -35,7 +39,7 @@ public class EnemyAI : MonoBehaviour
|
|||||||
|
|
||||||
[Header("Combat State")]
|
[Header("Combat State")]
|
||||||
public bool playerHasArtifact;
|
public bool playerHasArtifact;
|
||||||
public bool isAggroedBySound; // <-- MỚI: Trạng thái bị đánh động bởi âm thanh
|
public bool isAggroedBySound;
|
||||||
public GameObject laserPrefab;
|
public GameObject laserPrefab;
|
||||||
public Transform firePoint;
|
public Transform firePoint;
|
||||||
public float minShootDelay = 1.5f;
|
public float minShootDelay = 1.5f;
|
||||||
@@ -49,12 +53,20 @@ 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)")]
|
[Header("Advanced AI States (New)")]
|
||||||
|
public bool isPanicking = false;
|
||||||
|
public bool isEnraged = false;
|
||||||
|
public float panicHealthThreshold = 40f; // Dưới 40% máu sẽ panic
|
||||||
|
public float regenRate = 2f; // Hồi 2 máu mỗi giây
|
||||||
|
public float regenDelay = 5f; // Cần 5s không nhận dame để bắt đầu hồi
|
||||||
|
private float lastDamageTime;
|
||||||
|
|
||||||
|
[Header("Artifact Combat Upgrades")]
|
||||||
public float minStrafeDuration = 0.5f;
|
public float minStrafeDuration = 0.5f;
|
||||||
public float maxStrafeDuration = 2.2f;
|
public float maxStrafeDuration = 2.2f;
|
||||||
public float maxSpreadAngle = 6f;
|
public float maxSpreadAngle = 6f;
|
||||||
public float burstInterval = 0.12f;
|
public float burstInterval = 0.12f;
|
||||||
|
|
||||||
public float approachWeight = 0.35f;
|
public float approachWeight = 0.35f;
|
||||||
public float minCombatDistance = 5.0f;
|
public float minCombatDistance = 5.0f;
|
||||||
|
|
||||||
@@ -79,8 +91,8 @@ public class EnemyAI : MonoBehaviour
|
|||||||
public float suspicionLevel = 0f;
|
public float suspicionLevel = 0f;
|
||||||
public float investigationThreshold = 30f;
|
public float investigationThreshold = 30f;
|
||||||
public float alertNeighborsThreshold = 70f;
|
public float alertNeighborsThreshold = 70f;
|
||||||
public float alertRange = 20f;
|
public float alertRange = 25f; // Tăng tầm gọi hội
|
||||||
|
|
||||||
public Node rootNode;
|
public Node rootNode;
|
||||||
|
|
||||||
void Start()
|
void Start()
|
||||||
@@ -90,18 +102,15 @@ public class EnemyAI : MonoBehaviour
|
|||||||
fov = GetComponent<FieldOfView>();
|
fov = GetComponent<FieldOfView>();
|
||||||
chatBubble = GetComponentInChildren<Hallucinate.UI.ChatBubble>(true);
|
chatBubble = GetComponentInChildren<Hallucinate.UI.ChatBubble>(true);
|
||||||
mainCollider = GetComponent<Collider>();
|
mainCollider = GetComponent<Collider>();
|
||||||
|
health = GetComponent<vHealthController>();
|
||||||
|
|
||||||
// Tự động gán Layer Enemy nếu chưa có
|
// Thiết lập Invector Health
|
||||||
|
health.onReceiveDamage.AddListener(OnReceiveDamage);
|
||||||
|
health.onDead.AddListener(OnDead);
|
||||||
|
|
||||||
|
// Tự động gán Layer Enemy
|
||||||
if (gameObject.layer == LayerMask.NameToLayer("Default"))
|
if (gameObject.layer == LayerMask.NameToLayer("Default"))
|
||||||
{
|
|
||||||
gameObject.layer = LayerMask.NameToLayer("Enemy");
|
gameObject.layer = LayerMask.NameToLayer("Enemy");
|
||||||
Debug.Log($"[AI {npcName}] Tự động chuyển Layer sang Enemy để súng có thể bắn trúng.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mainCollider == null)
|
|
||||||
{
|
|
||||||
Debug.LogError($"[AI {npcName}] THIẾU COLLIDER! NPC này sẽ không thể bị bắn trúng.");
|
|
||||||
}
|
|
||||||
|
|
||||||
rb.isKinematic = true;
|
rb.isKinematic = true;
|
||||||
rb.freezeRotation = true;
|
rb.freezeRotation = true;
|
||||||
@@ -118,11 +127,16 @@ public class EnemyAI : MonoBehaviour
|
|||||||
|
|
||||||
void InitTree()
|
void InitTree()
|
||||||
{
|
{
|
||||||
|
// 1. Phản xạ tức thì (Dodge)
|
||||||
var dodgeSequence = new Sequence(new List<Node> { new TaskNode(CheckDodgeConditions), new TaskNode(ActionDodge) });
|
var dodgeSequence = new Sequence(new List<Node> { new TaskNode(CheckDodgeConditions), new TaskNode(ActionDodge) });
|
||||||
|
|
||||||
|
// 2. Hoảng loạn (Panic)
|
||||||
|
var panicSequence = new Sequence(new List<Node> { new TaskNode(CheckPanicConditions), new TaskNode(ActionPanic) });
|
||||||
|
|
||||||
// Đổi hàm CheckHasArtifact thành CheckCombatConditions để dùng chung cho cả âm thanh
|
// 3. Tấn công (Laser)
|
||||||
var laserSequence = new Sequence(new List<Node> { new TaskNode(CheckCombatConditions), new TaskNode(ActionFocusAndShoot) });
|
var laserSequence = new Sequence(new List<Node> { new TaskNode(CheckCombatConditions), new TaskNode(ActionFocusAndShoot) });
|
||||||
|
|
||||||
|
// 4. Các hành vi khác
|
||||||
var chaseSequence = new Sequence(new List<Node> { new TaskNode(CheckCanSeePlayer), new TaskNode(ActionChasePlayer) });
|
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 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) });
|
var talkSequence = new Sequence(new List<Node> { new TaskNode(CheckCanTalkToNPC), new TaskNode(ActionTalk) });
|
||||||
@@ -131,6 +145,7 @@ public class EnemyAI : MonoBehaviour
|
|||||||
rootNode = new Selector(new List<Node>
|
rootNode = new Selector(new List<Node>
|
||||||
{
|
{
|
||||||
dodgeSequence,
|
dodgeSequence,
|
||||||
|
panicSequence,
|
||||||
laserSequence,
|
laserSequence,
|
||||||
chaseSequence,
|
chaseSequence,
|
||||||
investigateSequence,
|
investigateSequence,
|
||||||
@@ -141,47 +156,115 @@ public class EnemyAI : MonoBehaviour
|
|||||||
|
|
||||||
void Update()
|
void Update()
|
||||||
{
|
{
|
||||||
if (player == null) return;
|
if (player == null || health.isDead) return;
|
||||||
|
|
||||||
// Đảm bảo Collider luôn bật
|
// Đảm bảo Collider luôn bật
|
||||||
if (mainCollider != null && !mainCollider.enabled)
|
if (mainCollider != null && !mainCollider.enabled) mainCollider.enabled = true;
|
||||||
{
|
|
||||||
mainCollider.enabled = true;
|
|
||||||
Debug.LogWarning($"[AI {npcName}] Collider bị tắt trái phép! Đã tự động bật lại.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!agent.isOnNavMesh) return;
|
HandleHealthRegen();
|
||||||
|
|
||||||
suspicionLevel = Mathf.Max(0, suspicionLevel - Time.deltaTime * 0.5f);
|
suspicionLevel = Mathf.Max(0, suspicionLevel - Time.deltaTime * 0.5f);
|
||||||
|
|
||||||
// Bình tĩnh lại và tắt chế độ bắn dồn dập khi mức độ nghi ngờ về 0
|
if (suspicionLevel <= 0f && !isEnraged) isAggroedBySound = false;
|
||||||
if (suspicionLevel <= 0f)
|
|
||||||
{
|
|
||||||
isAggroedBySound = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isTalking && !isDodging && agent.isStopped)
|
if (!isTalking && !isDodging && !isPanicking && agent.isStopped)
|
||||||
agent.isStopped = false;
|
agent.isStopped = false;
|
||||||
|
|
||||||
rootNode?.Evaluate();
|
rootNode?.Evaluate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void HandleHealthRegen()
|
||||||
|
{
|
||||||
|
if (Time.time > lastDamageTime + regenDelay && health.currentHealth < health.maxHealth)
|
||||||
|
{
|
||||||
|
health.AddHealth((int)(regenRate * Time.deltaTime));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#region HEALTH EVENTS
|
||||||
|
|
||||||
|
private void OnReceiveDamage(vDamage damage)
|
||||||
|
{
|
||||||
|
lastDamageTime = Time.time;
|
||||||
|
isAggroedBySound = true;
|
||||||
|
suspicionLevel = 100f;
|
||||||
|
StopConversation();
|
||||||
|
|
||||||
|
// Gọi hội ngay khi bị bắn
|
||||||
|
AlertNeighbors(damage.hitPosition);
|
||||||
|
|
||||||
|
// Né đòn ngay lập tức khi bị trúng dame (phản xạ Elden Ring)
|
||||||
|
if (Time.time > nextDodgeTime && !isDodging)
|
||||||
|
{
|
||||||
|
StartCoroutine(DodgeRollRoutine());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kiểm tra Enrage (Phase 2)
|
||||||
|
if (health.currentHealth < health.maxHealth * 0.5f && !isEnraged)
|
||||||
|
{
|
||||||
|
EnterEnrageMode();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDead(GameObject killer)
|
||||||
|
{
|
||||||
|
Debug.Log($"<color=black>[AI {npcName}] DIED.</color>");
|
||||||
|
// Khi chết, làm những con xung quanh Enraged gắt hơn
|
||||||
|
Collider[] hitColliders = Physics.OverlapSphere(transform.position, alertRange);
|
||||||
|
foreach (var hit in hitColliders)
|
||||||
|
{
|
||||||
|
EnemyAI ally = hit.GetComponentInParent<EnemyAI>();
|
||||||
|
if (ally != null && ally != this) ally.EnterEnrageMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.enabled = false;
|
||||||
|
this.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EnterEnrageMode()
|
||||||
|
{
|
||||||
|
if (isEnraged) return;
|
||||||
|
isEnraged = true;
|
||||||
|
isPanicking = false; // Hết sợ, chuyển sang liều mạng
|
||||||
|
|
||||||
|
// Buff stats mạnh mẽ như Elden Ring boss phase 2
|
||||||
|
moveSpeed *= 1.5f;
|
||||||
|
minShootDelay *= 0.5f;
|
||||||
|
maxShootDelay *= 0.5f;
|
||||||
|
maxSpreadAngle *= 0.6f; // Bắn chính xác hơn
|
||||||
|
approachWeight = 0.8f; // Áp sát cực gắt
|
||||||
|
minCombatDistance = 2.0f; // Đứng sát mặt Player để bắn
|
||||||
|
|
||||||
|
if (chatBubble != null) chatBubble.Show("FOR THE BROTHERHOOD! DIE!", 3f);
|
||||||
|
Debug.Log($"<color=red>[AI {npcName}] ENRAGED!</color> Stats boosted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region CONDITIONS
|
#region CONDITIONS
|
||||||
|
|
||||||
private NodeState CheckDodgeConditions()
|
private NodeState CheckDodgeConditions()
|
||||||
{
|
{
|
||||||
if (playerHasArtifact || isAggroedBySound) return NodeState.Failure;
|
|
||||||
|
|
||||||
if (isDodging) return NodeState.Success;
|
if (isDodging) return NodeState.Success;
|
||||||
if (fov != null && fov.canSeePlayer && Mouse.current.leftButton.isPressed)
|
// Tự né khi thấy Player click chuột trái (đang bắn)
|
||||||
|
if (fov != null && fov.canSeePlayer && Mouse.current.leftButton.isPressed && Time.time > nextDodgeTime)
|
||||||
return NodeState.Success;
|
return NodeState.Success;
|
||||||
return NodeState.Failure;
|
return NodeState.Failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Node này thay thế cho CheckHasArtifact cũ
|
private NodeState CheckPanicConditions()
|
||||||
|
{
|
||||||
|
if (isEnraged) return NodeState.Failure; // Đang điên thì không sợ
|
||||||
|
if (health.currentHealth < (health.maxHealth * (panicHealthThreshold / 100f)))
|
||||||
|
{
|
||||||
|
return NodeState.Success;
|
||||||
|
}
|
||||||
|
return NodeState.Failure;
|
||||||
|
}
|
||||||
|
|
||||||
private NodeState CheckCombatConditions()
|
private NodeState CheckCombatConditions()
|
||||||
{
|
{
|
||||||
bool shouldCombat = playerHasArtifact || isAggroedBySound;
|
bool shouldCombat = playerHasArtifact || isAggroedBySound || isEnraged;
|
||||||
if (shouldCombat) StopConversation();
|
if (shouldCombat) StopConversation();
|
||||||
return shouldCombat ? NodeState.Success : NodeState.Failure;
|
return shouldCombat ? NodeState.Success : NodeState.Failure;
|
||||||
}
|
}
|
||||||
@@ -189,48 +272,32 @@ 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(); AlertNeighbors(); suspicionLevel = 100; }
|
if (canSee) { StopConversation(); AlertNeighbors(transform.position); suspicionLevel = 100; }
|
||||||
return canSee ? NodeState.Success : NodeState.Failure;
|
return canSee ? NodeState.Success : NodeState.Failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
private NodeState CheckHasInvestigateTarget()
|
private NodeState CheckHasInvestigateTarget()
|
||||||
{
|
{
|
||||||
if (fov != null && fov.lastKnownPlayerPosition != Vector3.zero)
|
if (fov != null && fov.lastKnownPlayerPosition != Vector3.zero && suspicionLevel > investigationThreshold)
|
||||||
{
|
return NodeState.Success;
|
||||||
if (suspicionLevel > investigationThreshold)
|
|
||||||
{
|
|
||||||
if (Random.value < (suspicionLevel / 100f)) return NodeState.Success;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return NodeState.Failure;
|
return NodeState.Failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
private NodeState CheckCanTalkToNPC()
|
private NodeState CheckCanTalkToNPC()
|
||||||
{
|
{
|
||||||
if (playerHasArtifact || isAggroedBySound || (fov != null && fov.canSeePlayer)) return NodeState.Failure;
|
if (playerHasArtifact || isAggroedBySound || isEnraged || (fov != null && fov.canSeePlayer)) return NodeState.Failure;
|
||||||
if (Time.time < lastTalkTime + talkCooldown) return NodeState.Failure;
|
if (Time.time < lastTalkTime + talkCooldown || isTalking) return NodeState.Failure;
|
||||||
if (isTalking) return NodeState.Success;
|
if (Hallucinate.AI.ConversationManager.Instance == null || !Hallucinate.AI.ConversationManager.Instance.CanStartConversation()) return NodeState.Failure;
|
||||||
|
|
||||||
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)
|
||||||
{
|
{
|
||||||
if (hit.gameObject == gameObject) continue;
|
if (hit.gameObject == gameObject) continue;
|
||||||
|
|
||||||
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 && !other.isEnraged)
|
||||||
{
|
{
|
||||||
float dist = Vector3.Distance(transform.position, other.transform.position);
|
if (gameObject.GetInstanceID() < other.gameObject.GetInstanceID())
|
||||||
if (dist <= talkRange && gameObject.GetInstanceID() < other.gameObject.GetInstanceID())
|
|
||||||
{
|
{
|
||||||
Debug.Log($"<color=green>[AI {npcName}]</color> Found partner: {other.npcName}. Starting conversation.");
|
|
||||||
Hallucinate.AI.ConversationManager.Instance.StartConversation(this, other);
|
Hallucinate.AI.ConversationManager.Instance.StartConversation(this, other);
|
||||||
return NodeState.Success;
|
return NodeState.Success;
|
||||||
}
|
}
|
||||||
@@ -247,29 +314,12 @@ public class EnemyAI : MonoBehaviour
|
|||||||
{
|
{
|
||||||
suspicionLevel += volume * 15f;
|
suspicionLevel += volume * 15f;
|
||||||
if (fov != null) fov.lastKnownPlayerPosition = location;
|
if (fov != null) fov.lastKnownPlayerPosition = location;
|
||||||
|
if (suspicionLevel >= investigationThreshold) isAggroedBySound = true;
|
||||||
// Nếu AI nghe thấy tiếng động làm mức nghi ngờ vượt mốc điều tra, chuyển sang trạng thái chiến đấu
|
if (suspicionLevel >= alertNeighborsThreshold) AlertNeighbors(location);
|
||||||
if (suspicionLevel >= investigationThreshold)
|
|
||||||
{
|
|
||||||
isAggroedBySound = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (suspicionLevel >= alertNeighborsThreshold) AlertNeighbors();
|
|
||||||
StopConversation();
|
StopConversation();
|
||||||
Debug.Log($"<color=orange>[AI {npcName}]</color> Heard noise! Suspicion: {suspicionLevel}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void TriggerCombatAlert(Vector3 sourceLocation)
|
public void AlertNeighbors(Vector3 threatPos)
|
||||||
{
|
|
||||||
suspicionLevel = 100f;
|
|
||||||
isAggroedBySound = true; // Ép vào trạng thái tấn công
|
|
||||||
if (fov != null) fov.lastKnownPlayerPosition = sourceLocation;
|
|
||||||
StopConversation();
|
|
||||||
AlertNeighbors();
|
|
||||||
Debug.Log($"<color=red>[AI {npcName}] GUNFIRE DETECTED!</color> Entering combat mode.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AlertNeighbors()
|
|
||||||
{
|
{
|
||||||
Collider[] hitColliders = Physics.OverlapSphere(transform.position, alertRange);
|
Collider[] hitColliders = Physics.OverlapSphere(transform.position, alertRange);
|
||||||
foreach (var hit in hitColliders)
|
foreach (var hit in hitColliders)
|
||||||
@@ -277,31 +327,185 @@ public class EnemyAI : MonoBehaviour
|
|||||||
EnemyAI neighbor = hit.GetComponentInParent<EnemyAI>();
|
EnemyAI neighbor = hit.GetComponentInParent<EnemyAI>();
|
||||||
if (neighbor != null && neighbor != this)
|
if (neighbor != null && neighbor != this)
|
||||||
{
|
{
|
||||||
neighbor.suspicionLevel = Mathf.Max(neighbor.suspicionLevel, 50f);
|
neighbor.TriggerCombatAlert(threatPos);
|
||||||
if (fov != null && neighbor.fov != null) neighbor.fov.lastKnownPlayerPosition = fov.lastKnownPlayerPosition;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void TriggerCombatAlert(Vector3 sourceLocation)
|
||||||
|
{
|
||||||
|
if (isEnraged) return;
|
||||||
|
suspicionLevel = 100f;
|
||||||
|
isAggroedBySound = true;
|
||||||
|
if (fov != null) fov.lastKnownPlayerPosition = sourceLocation;
|
||||||
|
StopConversation();
|
||||||
|
}
|
||||||
|
|
||||||
|
private NodeState ActionPanic()
|
||||||
|
{
|
||||||
|
isPanicking = true;
|
||||||
|
agent.speed = moveSpeed * 1.3f;
|
||||||
|
|
||||||
|
// Chạy trốn khỏi Player đến một điểm ngẫu nhiên xa Player
|
||||||
|
if (!agent.pathPending && agent.remainingDistance < 1f)
|
||||||
|
{
|
||||||
|
Vector3 runDir = (transform.position - player.position).normalized;
|
||||||
|
Vector3 escapePoint = transform.position + runDir * 10f + Random.insideUnitSphere * 5f;
|
||||||
|
|
||||||
|
NavMeshHit hit;
|
||||||
|
if (NavMesh.SamplePosition(escapePoint, out hit, 10f, 1))
|
||||||
|
agent.SetDestination(hit.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chatBubble != null && Random.value < 0.01f) chatBubble.Show("HELP! HE'S KILLING US!", 1f);
|
||||||
|
|
||||||
|
return NodeState.Running;
|
||||||
|
}
|
||||||
|
|
||||||
private NodeState ActionTalk()
|
private NodeState ActionTalk()
|
||||||
{
|
{
|
||||||
if (isTalking)
|
if (isTalking) { agent.isStopped = true; return NodeState.Running; }
|
||||||
{
|
|
||||||
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;
|
return NodeState.Failure;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void StopConversation()
|
||||||
|
{
|
||||||
|
if (isTalking && Hallucinate.AI.ConversationManager.Instance != null)
|
||||||
|
{
|
||||||
|
Hallucinate.AI.ConversationManager.Instance.InterruptConversation(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private NodeState ActionPatrol()
|
||||||
|
{
|
||||||
|
isPanicking = false;
|
||||||
|
agent.isStopped = false;
|
||||||
|
agent.speed = patrolSpeed;
|
||||||
|
if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance)
|
||||||
|
{
|
||||||
|
currentWaitTime += Time.deltaTime;
|
||||||
|
if (currentWaitTime >= patrolWaitTime)
|
||||||
|
{
|
||||||
|
Vector3 randomDest = startPosition + Random.insideUnitSphere * patrolRadius;
|
||||||
|
NavMeshHit hit;
|
||||||
|
if (NavMesh.SamplePosition(randomDest, out hit, patrolRadius, 1)) agent.SetDestination(hit.position);
|
||||||
|
currentWaitTime = 0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return NodeState.Running;
|
||||||
|
}
|
||||||
|
|
||||||
|
private NodeState ActionChasePlayer()
|
||||||
|
{
|
||||||
|
isPanicking = false;
|
||||||
|
agent.isStopped = false;
|
||||||
|
agent.speed = moveSpeed;
|
||||||
|
agent.SetDestination(player.position);
|
||||||
|
return NodeState.Running;
|
||||||
|
}
|
||||||
|
|
||||||
|
private NodeState ActionInvestigate()
|
||||||
|
{
|
||||||
|
agent.isStopped = false;
|
||||||
|
agent.speed = moveSpeed * 0.7f;
|
||||||
|
agent.SetDestination(fov.lastKnownPlayerPosition);
|
||||||
|
if (Vector3.Distance(transform.position, fov.lastKnownPlayerPosition) < 1.5f)
|
||||||
|
{
|
||||||
|
currentWaitTime += Time.deltaTime;
|
||||||
|
if (currentWaitTime > 3f) { fov.lastKnownPlayerPosition = Vector3.zero; return NodeState.Success; }
|
||||||
|
}
|
||||||
|
return NodeState.Running;
|
||||||
|
}
|
||||||
|
|
||||||
|
private NodeState ActionFocusAndShoot()
|
||||||
|
{
|
||||||
|
isPanicking = false;
|
||||||
|
if (player == null) return NodeState.Failure;
|
||||||
|
if (agent.hasPath) agent.ResetPath();
|
||||||
|
agent.isStopped = false;
|
||||||
|
|
||||||
|
Vector3 targetPos = player.position;
|
||||||
|
if (!playerHasArtifact && fov != null && !fov.canSeePlayer && fov.lastKnownPlayerPosition != Vector3.zero)
|
||||||
|
targetPos = fov.lastKnownPlayerPosition;
|
||||||
|
|
||||||
|
// Xoay về phía mục tiêu
|
||||||
|
Vector3 bodyDir = (targetPos - transform.position);
|
||||||
|
bodyDir.y = 0f;
|
||||||
|
if (bodyDir != Vector3.zero)
|
||||||
|
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(bodyDir), rotateSpeed * Time.deltaTime);
|
||||||
|
|
||||||
|
// Strafe di chuyển linh hoạt
|
||||||
|
if (Time.time >= nextStrafeChangeTime)
|
||||||
|
{
|
||||||
|
strafeDirectionSign = new int[] { -1, 1, 0 }[Random.Range(0, 3)];
|
||||||
|
nextStrafeChangeTime = Time.time + Random.Range(minStrafeDuration, maxStrafeDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 moveDir = Vector3.zero;
|
||||||
|
if (strafeDirectionSign != 0 && bodyDir != Vector3.zero)
|
||||||
|
{
|
||||||
|
Vector3 normal = bodyDir.normalized;
|
||||||
|
moveDir = new Vector3(-normal.z, 0, normal.x) * strafeDirectionSign;
|
||||||
|
if (Vector3.Distance(transform.position, targetPos) > minCombatDistance) moveDir += normal * approachWeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (moveDir != Vector3.zero)
|
||||||
|
{
|
||||||
|
agent.speed = moveSpeed * (isEnraged ? 1f : 0.75f);
|
||||||
|
agent.Move(moveDir.normalized * agent.speed * Time.deltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (firePoint != null)
|
||||||
|
firePoint.rotation = Quaternion.LookRotation((targetPos + Vector3.up * 1f) - firePoint.position);
|
||||||
|
|
||||||
|
if (Time.time >= nextShootTime && !isShootingBurst)
|
||||||
|
{
|
||||||
|
StartCoroutine(ShootBurstRoutine(Random.Range(1, isEnraged ? 6 : 4)));
|
||||||
|
nextShootTime = Time.time + Random.Range(minShootDelay, maxShootDelay);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NodeState.Running;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator ShootBurstRoutine(int bulletCount)
|
||||||
|
{
|
||||||
|
isShootingBurst = true;
|
||||||
|
for (int i = 0; i < bulletCount; i++)
|
||||||
|
{
|
||||||
|
if (laserPrefab == null || firePoint == null) break;
|
||||||
|
float spread = isEnraged ? maxSpreadAngle * 0.5f : maxSpreadAngle;
|
||||||
|
Quaternion rot = firePoint.rotation * Quaternion.Euler(Random.Range(-spread, spread), Random.Range(-spread, spread), 0f);
|
||||||
|
Instantiate(laserPrefab, firePoint.position, rot);
|
||||||
|
yield return new WaitForSeconds(isEnraged ? burstInterval * 0.7f : burstInterval);
|
||||||
|
}
|
||||||
|
isShootingBurst = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private NodeState ActionDodge()
|
||||||
|
{
|
||||||
|
if (!isDodging) StartCoroutine(DodgeRollRoutine());
|
||||||
|
return NodeState.Running;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerator DodgeRollRoutine()
|
||||||
|
{
|
||||||
|
isDodging = true;
|
||||||
|
agent.enabled = false;
|
||||||
|
rb.isKinematic = false;
|
||||||
|
if (mainCollider != null) mainCollider.enabled = true;
|
||||||
|
|
||||||
|
Vector3 dir = (player.position - transform.position).normalized;
|
||||||
|
Vector3 perp = new Vector3(-dir.z, 0, dir.x) * (Random.value > 0.5f ? 1 : -1);
|
||||||
|
rb.AddForce(perp * (isEnraged ? dodgeForce * 1.5f : dodgeForce), ForceMode.Impulse);
|
||||||
|
|
||||||
|
yield return new WaitForSeconds(dodgeDuration);
|
||||||
|
rb.linearVelocity = Vector3.zero;
|
||||||
|
rb.isKinematic = true;
|
||||||
|
agent.enabled = true;
|
||||||
|
nextDodgeTime = Time.time + (isEnraged ? dodgeCooldown * 0.5f : dodgeCooldown);
|
||||||
|
isDodging = false;
|
||||||
|
}
|
||||||
|
|
||||||
public void ProcessDialogueResult(string json)
|
public void ProcessDialogueResult(string json)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -316,209 +520,6 @@ public class EnemyAI : MonoBehaviour
|
|||||||
catch { if (chatBubble != null) chatBubble.Show(json); }
|
catch { if (chatBubble != null) chatBubble.Show(json); }
|
||||||
}
|
}
|
||||||
|
|
||||||
private void StopConversation()
|
|
||||||
{
|
|
||||||
if (isTalking && Hallucinate.AI.ConversationManager.Instance != null)
|
|
||||||
{
|
|
||||||
Hallucinate.AI.ConversationManager.Instance.InterruptConversation(this);
|
|
||||||
if (chatBubble != null) chatBubble.Show("Wait, what was that?!", 2f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private NodeState ActionPatrol()
|
|
||||||
{
|
|
||||||
agent.isStopped = false;
|
|
||||||
agent.speed = patrolSpeed;
|
|
||||||
|
|
||||||
if (!agent.pathPending && agent.remainingDistance <= agent.stoppingDistance)
|
|
||||||
{
|
|
||||||
currentWaitTime += Time.deltaTime;
|
|
||||||
|
|
||||||
if (currentWaitTime >= patrolWaitTime)
|
|
||||||
{
|
|
||||||
Vector3 randomDirection = Random.insideUnitSphere * patrolRadius;
|
|
||||||
randomDirection += startPosition;
|
|
||||||
|
|
||||||
NavMeshHit hit;
|
|
||||||
if (NavMesh.SamplePosition(randomDirection, out hit, patrolRadius, 1))
|
|
||||||
{
|
|
||||||
agent.SetDestination(hit.position);
|
|
||||||
}
|
|
||||||
|
|
||||||
currentWaitTime = 0f;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NodeState.Running;
|
|
||||||
}
|
|
||||||
|
|
||||||
private NodeState ActionChasePlayer()
|
|
||||||
{
|
|
||||||
agent.isStopped = false;
|
|
||||||
agent.speed = moveSpeed;
|
|
||||||
agent.SetDestination(player.position);
|
|
||||||
return NodeState.Running;
|
|
||||||
}
|
|
||||||
|
|
||||||
private NodeState ActionInvestigate()
|
|
||||||
{
|
|
||||||
agent.isStopped = false;
|
|
||||||
agent.speed = moveSpeed * 0.7f;
|
|
||||||
agent.SetDestination(fov.lastKnownPlayerPosition);
|
|
||||||
|
|
||||||
if (Vector3.Distance(transform.position, fov.lastKnownPlayerPosition) < 1.5f)
|
|
||||||
{
|
|
||||||
currentWaitTime += Time.deltaTime;
|
|
||||||
if (currentWaitTime > 3f)
|
|
||||||
{
|
|
||||||
fov.lastKnownPlayerPosition = Vector3.zero;
|
|
||||||
suspicionLevel *= 0.5f;
|
|
||||||
return NodeState.Success;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return NodeState.Running;
|
|
||||||
}
|
|
||||||
|
|
||||||
private NodeState ActionFocusAndShoot()
|
|
||||||
{
|
|
||||||
if (player == null) return NodeState.Failure;
|
|
||||||
|
|
||||||
if (agent.hasPath) agent.ResetPath();
|
|
||||||
agent.isStopped = false;
|
|
||||||
|
|
||||||
// CẢI TIẾN: Xác định mục tiêu linh hoạt để tránh Wallhack.
|
|
||||||
Vector3 targetPos = player.position; // Mặc định khóa chặt người chơi
|
|
||||||
|
|
||||||
// Nếu không cầm Artifact và cũng chưa bị nhìn thấy, AI chỉ nhắm vào MỐC ÂM THANH
|
|
||||||
if (!playerHasArtifact && fov != null && !fov.canSeePlayer && fov.lastKnownPlayerPosition != Vector3.zero)
|
|
||||||
{
|
|
||||||
targetPos = fov.lastKnownPlayerPosition;
|
|
||||||
|
|
||||||
// Nếu AI tiến hành áp sát và xả đạn vào nơi phát ra tiếng mà không thấy ai, ngưng bắn
|
|
||||||
if (Vector3.Distance(transform.position, targetPos) < 2f)
|
|
||||||
{
|
|
||||||
isAggroedBySound = false;
|
|
||||||
suspicionLevel *= 0.5f;
|
|
||||||
return NodeState.Success;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. XOAY THÂN THEO TRỤC NGANG HƯỚNG VỀ MỤC TIÊU (Người chơi hoặc Tiếng động)
|
|
||||||
Vector3 bodyDir = targetPos - transform.position;
|
|
||||||
bodyDir.y = 0f;
|
|
||||||
Vector3 bodyDirNormal = bodyDir.normalized;
|
|
||||||
if (bodyDir != Vector3.zero)
|
|
||||||
{
|
|
||||||
Quaternion targetRotation = Quaternion.LookRotation(bodyDirNormal);
|
|
||||||
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, rotateSpeed * Time.deltaTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. RANDOM THỜI GIAN/KHOẢNG CÁCH DUY TRÌ HƯỚNG DI CHUYỂN
|
|
||||||
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)];
|
|
||||||
nextStrafeChangeTime = Time.time + Random.Range(minStrafeDuration, maxStrafeDuration);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. TÍNH TOÁN TRỘN VECTOR: CHỈ TIẾN LÊN KHI ĐANG ĐI NGANG TRÁI/PHẢI
|
|
||||||
Vector3 finalMovementVector = Vector3.zero;
|
|
||||||
|
|
||||||
if (strafeDirectionSign != 0 && bodyDir != Vector3.zero)
|
|
||||||
{
|
|
||||||
finalMovementVector = new Vector3(-bodyDirNormal.z, 0, bodyDirNormal.x) * strafeDirectionSign;
|
|
||||||
|
|
||||||
float currentDistance = Vector3.Distance(transform.position, targetPos);
|
|
||||||
if (currentDistance > minCombatDistance)
|
|
||||||
{
|
|
||||||
finalMovementVector += bodyDirNormal * approachWeight;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (finalMovementVector != Vector3.zero)
|
|
||||||
{
|
|
||||||
finalMovementVector.Normalize();
|
|
||||||
agent.speed = moveSpeed * 0.75f;
|
|
||||||
agent.Move(finalMovementVector * agent.speed * Time.deltaTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. XOAY HỌNG SÚNG TRỤC DỌC NHẮM VÀO MỤC TIÊU
|
|
||||||
if (firePoint != null)
|
|
||||||
{
|
|
||||||
Vector3 targetCenter = targetPos + Vector3.up * 1f;
|
|
||||||
Vector3 aimDir = targetCenter - firePoint.position;
|
|
||||||
if (aimDir != Vector3.zero)
|
|
||||||
{
|
|
||||||
firePoint.rotation = Quaternion.LookRotation(aimDir);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. RANDOM SỐ ĐẠN & DELAY GIỮA CÁC ĐỢT BẮN
|
|
||||||
if (Time.time >= nextShootTime && !isShootingBurst)
|
|
||||||
{
|
|
||||||
int randomBulletCount = Random.Range(1, 4);
|
|
||||||
StartCoroutine(ShootBurstRoutine(randomBulletCount));
|
|
||||||
nextShootTime = Time.time + Random.Range(minShootDelay, maxShootDelay);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NodeState.Running;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerator ShootBurstRoutine(int bulletCount)
|
|
||||||
{
|
|
||||||
isShootingBurst = true;
|
|
||||||
|
|
||||||
for (int i = 0; i < bulletCount; i++)
|
|
||||||
{
|
|
||||||
if (laserPrefab == null || firePoint == null) break;
|
|
||||||
|
|
||||||
float randomX = Random.Range(-maxSpreadAngle, maxSpreadAngle);
|
|
||||||
float randomY = Random.Range(-maxSpreadAngle, maxSpreadAngle);
|
|
||||||
Quaternion spreadRotation = Quaternion.Euler(randomX, randomY, 0f);
|
|
||||||
|
|
||||||
Quaternion finalBulletRotation = firePoint.rotation * spreadRotation;
|
|
||||||
|
|
||||||
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}");
|
|
||||||
|
|
||||||
if (i < bulletCount - 1)
|
|
||||||
{
|
|
||||||
yield return new WaitForSeconds(burstInterval);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isShootingBurst = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ShootLaser() { }
|
|
||||||
|
|
||||||
private NodeState ActionDodge()
|
|
||||||
{
|
|
||||||
if (!isDodging) StartCoroutine(DodgeRollRoutine());
|
|
||||||
return NodeState.Running;
|
|
||||||
}
|
|
||||||
|
|
||||||
private IEnumerator DodgeRollRoutine()
|
|
||||||
{
|
|
||||||
isDodging = true;
|
|
||||||
agent.enabled = false;
|
|
||||||
rb.isKinematic = false;
|
|
||||||
|
|
||||||
// GIỮ COLLIDER LUÔN BẬT KHI NÉ
|
|
||||||
if (mainCollider != null) mainCollider.enabled = true;
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetTalkingPartner(EnemyAI partner) { talkingPartner = partner; }
|
public void SetTalkingPartner(EnemyAI partner) { talkingPartner = partner; }
|
||||||
public void FaceTarget(Vector3 pos)
|
public void FaceTarget(Vector3 pos)
|
||||||
{
|
{
|
||||||
@@ -531,13 +532,7 @@ public class EnemyAI : MonoBehaviour
|
|||||||
|
|
||||||
private void OnDrawGizmos()
|
private void OnDrawGizmos()
|
||||||
{
|
{
|
||||||
Gizmos.color = Color.green;
|
Gizmos.color = isEnraged ? Color.red : Color.green;
|
||||||
Gizmos.DrawWireSphere(transform.position, talkRange);
|
Gizmos.DrawWireSphere(transform.position, alertRange);
|
||||||
|
|
||||||
if (isTalking && talkingPartner != null)
|
|
||||||
{
|
|
||||||
Gizmos.color = Color.yellow;
|
|
||||||
Gizmos.DrawLine(transform.position + Vector3.up, talkingPartner.transform.position + Vector3.up);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user