diff --git a/Assets/Tests/EditMode/AI/UnawareModuleTests.cs b/Assets/Tests/EditMode/AI/UnawareModuleTests.cs
new file mode 100644
index 00000000..7356356a
--- /dev/null
+++ b/Assets/Tests/EditMode/AI/UnawareModuleTests.cs
@@ -0,0 +1,70 @@
+using NUnit.Framework;
+using BaseGames.AI;
+using BaseGames.Enemies;
+
+namespace BaseGames.Tests.EditMode.AI
+{
+ public class UnawareModuleTests
+ {
+ // 只跑未发现层自身(不挂骨架升级边),验证态、行为与内部边。
+ static AiRuntime RunAlone(IUnawareModule m, FakeAiContext ctx)
+ {
+ var b = new BrainBuilder();
+ m.Declare(b);
+ m.Link(b);
+ b.Entry(m.Entry);
+ return new AiRuntime(b.Build(), ctx);
+ }
+
+ [Test]
+ public void SinglePost_OneState_EntryEqualsRest()
+ {
+ var m = new SinglePost(LocomotionMode.Patrol);
+ Assert.AreEqual(1, m.States.Count);
+ Assert.AreEqual(m.Entry, m.Rest);
+ var ctx = new FakeAiContext();
+ RunAlone(m, ctx);
+ Assert.AreEqual(LocomotionMode.Patrol, ctx.L.CurrentMode);
+ }
+
+ [Test]
+ public void DisguiseThenPatrol_EntryIsDisguise_RestIsPatrol()
+ {
+ var m = new DisguiseThenPatrol();
+ Assert.AreEqual(DisguiseThenPatrol.Disguise, m.Entry);
+ Assert.AreEqual(DisguiseThenPatrol.Patrol, m.Rest);
+ CollectionAssert.AreEquivalent(
+ new[] { DisguiseThenPatrol.Disguise, DisguiseThenPatrol.Patrol }, m.States);
+ }
+
+ [Test]
+ public void DisguiseThenPatrol_HasNoInternalEdges_DisguiseIsOneWay()
+ {
+ var ctx = new FakeAiContext();
+ var rt = RunAlone(new DisguiseThenPatrol(), ctx);
+ Assert.AreEqual(DisguiseThenPatrol.Disguise, rt.CurrentStateName);
+ for (int i = 0; i < 20; i++) rt.Tick(1f);
+ // 无内部边:伪装态自己不会跑掉,只能被骨架的升级边带走
+ Assert.AreEqual(DisguiseThenPatrol.Disguise, rt.CurrentStateName);
+ Assert.AreEqual(LocomotionMode.Idle, ctx.L.CurrentMode);
+ }
+
+ [Test]
+ public void AlternatingIdlePatrol_SwitchesOnDwellTimers()
+ {
+ var m = new AlternatingIdlePatrol(idleDwell: 2f, patrolDwell: 3f);
+ var ctx = new FakeAiContext();
+ var rt = RunAlone(m, ctx);
+ Assert.AreEqual(AlternatingIdlePatrol.Idle, rt.CurrentStateName);
+
+ rt.Tick(1f);
+ Assert.AreEqual(AlternatingIdlePatrol.Idle, rt.CurrentStateName); // 未到 2s
+ rt.Tick(1.5f);
+ Assert.AreEqual(AlternatingIdlePatrol.Patrol, rt.CurrentStateName); // 累计 2.5s ≥ 2s
+ Assert.AreEqual(LocomotionMode.Patrol, ctx.L.CurrentMode);
+
+ rt.Tick(3.5f);
+ Assert.AreEqual(AlternatingIdlePatrol.Idle, rt.CurrentStateName); // ≥ 3s 回站立
+ }
+ }
+}
diff --git a/Assets/Tests/EditMode/AI/UnawareModuleTests.cs.meta b/Assets/Tests/EditMode/AI/UnawareModuleTests.cs.meta
new file mode 100644
index 00000000..e5d3e00f
--- /dev/null
+++ b/Assets/Tests/EditMode/AI/UnawareModuleTests.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: f24cb7c97b01d1e48bfad2dc149f8bdf
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware.meta b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware.meta
new file mode 100644
index 00000000..6db23741
--- /dev/null
+++ b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 897dfc19ae815c9428d471e354151724
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/AlternatingIdlePatrol.cs b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/AlternatingIdlePatrol.cs
new file mode 100644
index 00000000..8b853a81
--- /dev/null
+++ b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/AlternatingIdlePatrol.cs
@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+using BaseGames.AI;
+
+namespace BaseGames.Enemies
+{
+ /// 站立 ⇄ 巡逻定时交替,脱战回站立。内部计时边经 Link 挂载,优先级低于骨架升级边。
+ [Serializable]
+ public sealed class AlternatingIdlePatrol : IUnawareModule
+ {
+ public const string Idle = "Idle";
+ public const string Patrol = "Patrol";
+ static readonly string[] StatesArray = { Idle, Patrol };
+
+ [Tooltip("站立停留时长(秒),到点转巡逻")]
+ [SerializeField, Min(0.1f)] float _idleDwell = 2f;
+ [Tooltip("巡逻持续时长(秒),到点转站立")]
+ [SerializeField, Min(0.1f)] float _patrolDwell = 4f;
+
+ public AlternatingIdlePatrol() { }
+ public AlternatingIdlePatrol(float idleDwell, float patrolDwell)
+ { _idleDwell = idleDwell; _patrolDwell = patrolDwell; }
+
+ public string Entry => Idle;
+ public string Rest => Idle;
+ public IReadOnlyList States => StatesArray;
+
+ public void Declare(BrainBuilder b)
+ {
+ AiStateFragments.Locomotion(b, Idle, LocomotionMode.Idle);
+ AiStateFragments.Locomotion(b, Patrol, LocomotionMode.Patrol);
+ }
+
+ public void Link(BrainBuilder b)
+ {
+ b.State(Idle) .To(Patrol).After(_idleDwell);
+ b.State(Patrol).To(Idle) .After(_patrolDwell);
+ }
+ }
+}
diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/AlternatingIdlePatrol.cs.meta b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/AlternatingIdlePatrol.cs.meta
new file mode 100644
index 00000000..347b4835
--- /dev/null
+++ b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/AlternatingIdlePatrol.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: e6e5fc2afebbf144b95b40b1f01a038c
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/DisguiseThenPatrol.cs b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/DisguiseThenPatrol.cs
new file mode 100644
index 00000000..9978c8fa
--- /dev/null
+++ b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/DisguiseThenPatrol.cs
@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+using BaseGames.AI;
+
+namespace BaseGames.Enemies
+{
+ ///
+ /// 单向降级:出生时伪装静止,一旦交战过就只回巡逻,伪装态不可逆。
+ /// 刻意不声明两态之间的内部边——伪装只在出生那一次可达。
+ ///
+ [Serializable]
+ public sealed class DisguiseThenPatrol : IUnawareModule
+ {
+ public const string Disguise = "Disguise";
+ public const string Patrol = "Patrol";
+ static readonly string[] StatesArray = { Disguise, Patrol };
+
+ [Tooltip("出生伪装态的移动模式(通常 Idle)")]
+ [SerializeField] LocomotionMode _disguiseMode = LocomotionMode.Idle;
+ [Tooltip("脱战后常态的移动模式(通常 Patrol)")]
+ [SerializeField] LocomotionMode _patrolMode = LocomotionMode.Patrol;
+
+ public DisguiseThenPatrol() { }
+ public DisguiseThenPatrol(LocomotionMode disguiseMode, LocomotionMode patrolMode)
+ { _disguiseMode = disguiseMode; _patrolMode = patrolMode; }
+
+ public string Entry => Disguise;
+ public string Rest => Patrol;
+ public IReadOnlyList States => StatesArray;
+
+ public void Declare(BrainBuilder b)
+ {
+ AiStateFragments.Locomotion(b, Disguise, _disguiseMode);
+ AiStateFragments.Locomotion(b, Patrol, _patrolMode);
+ }
+
+ public void Link(BrainBuilder b) { } // 无内部边:伪装态单向不可逆
+ }
+}
diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/DisguiseThenPatrol.cs.meta b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/DisguiseThenPatrol.cs.meta
new file mode 100644
index 00000000..0a3ba093
--- /dev/null
+++ b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/DisguiseThenPatrol.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 92aa888ba3d46244bb0592cd8fd2860e
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/SinglePost.cs b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/SinglePost.cs
new file mode 100644
index 00000000..588ee897
--- /dev/null
+++ b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/SinglePost.cs
@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+using BaseGames.AI;
+
+namespace BaseGames.Enemies
+{
+ /// 单一未发现态:站桩(Idle)或单一巡逻(Patrol)。出生态与脱战态是同一个。
+ [Serializable]
+ public sealed class SinglePost : IUnawareModule
+ {
+ public const string Post = "Post";
+ static readonly string[] StatesArray = { Post };
+
+ [Tooltip("未发现时的移动模式:Idle=站着不动,Patrol=按配置策略游走")]
+ [SerializeField] LocomotionMode _mode = LocomotionMode.Patrol;
+
+ public SinglePost() { }
+ public SinglePost(LocomotionMode mode) { _mode = mode; }
+
+ public string Entry => Post;
+ public string Rest => Post;
+ public IReadOnlyList States => StatesArray;
+
+ public void Declare(BrainBuilder b) => AiStateFragments.Locomotion(b, Post, _mode);
+ public void Link(BrainBuilder b) { } // 单态,无内部边
+ }
+}
diff --git a/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/SinglePost.cs.meta b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/SinglePost.cs.meta
new file mode 100644
index 00000000..3be4a884
--- /dev/null
+++ b/Assets/_Game/Scripts/Enemies/AIBrain/Modules/Unaware/SinglePost.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 50f6c02637b111f44bbe3eea938871be
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant: