using System; using System.Collections.Generic; namespace BaseGames.AI { /// fluent builder:声明层的唯一入口。构建出不可变 AiGraph。 public sealed class BrainBuilder { readonly Dictionary _states = new Dictionary(); readonly List _globals = new List(); string _entry; public BrainBuilder Entry(string stateName) { _entry = stateName; return this; } /// /// 取一个状态,不存在则新建(宽松)。**只用于给别人已声明的态挂转换**。 /// 要声明一个态的行为(OnEnter/Tick/OnExit)请用 DeclareState()——它会挡住重名覆盖。 /// public StateBuilder State(string name) { if (!_states.TryGetValue(name, out var s)) { s = new AiState(name); _states[name] = s; } return new StateBuilder(s); } public GlobalBuilder Global() => new GlobalBuilder(this); /// /// 要求状态已被声明(含行为回调)。模块给"别人声明的态"挂边前调用。 /// 未声明即抛——否则 State() 会静默新建一个空态,敌人杵着不动且无任何报错。 /// public void RequireState(string name) { if (!_states.ContainsKey(name)) throw new InvalidOperationException( $"BrainBuilder: 状态 '{name}' 尚未声明。请先声明它的行为(AiStateFragments.* 或 State(name)),再挂转换。"); } /// /// 首次声明一个状态。名字已存在即抛——防止两个独立模块取了同名状态时, /// 后者的 OnEnter/Tick/OnExit 静默覆盖前者(敌人跑错行为且零报错)。 /// 只有"首次声明行为"的调用点用它;给已声明的态挂边仍用 State()。 /// public StateBuilder DeclareState(string name) { if (_states.ContainsKey(name)) throw new InvalidOperationException( $"BrainBuilder: 状态 '{name}' 已被声明过。两个模块取了同名状态会互相覆盖回调——" + "请给其中一个换个不冲突的名字。"); return State(name); } internal void AddGlobal(Transition t) => _globals.Add(t); /// /// 构建**完整**图。除了引用完整性,还校验「非终态必须有出口」。 /// 生产的两个装配点(AiScript.GetOrBuildGraph / AiRecipeSO)都走这里, /// 将来新增的入口默认即受保护——这正是校验放在 Build() 而非各入口的理由。 /// public AiGraph Build() => BuildInternal(checkDeadEnds: true); /// /// 构建**部分**图:跳过「非终态必须有出口」校验,其余校验照旧。 /// 单个模块(IUnawareModule / IEngagementModule)按设计只声明自己的态, /// 升级边由 PerceptionSkeleton 事后挂上——单独构建时的死角是正常中间态,不是缺陷。 /// 仅用于隔离验证单个模块或片段;装配真实敌人图一律用 。 /// public AiGraph BuildPartial() => BuildInternal(checkDeadEnds: false); AiGraph BuildInternal(bool checkDeadEnds) { if (string.IsNullOrEmpty(_entry)) throw new InvalidOperationException("BrainBuilder: 未设置 Entry 状态。"); if (!_states.ContainsKey(_entry)) throw new InvalidOperationException($"BrainBuilder: Entry 状态 '{_entry}' 未声明。"); foreach (var s in _states.Values) foreach (var t in s.Transitions) if (!_states.ContainsKey(t.Target)) throw new InvalidOperationException( $"BrainBuilder: 状态 '{s.Name}' 的转换指向未声明状态 '{t.Target}'。"); foreach (var t in _globals) if (!_states.ContainsKey(t.Target)) throw new InvalidOperationException( $"BrainBuilder: 全局转换指向未声明状态 '{t.Target}'。"); if (checkDeadEnds) { foreach (var s in _states.Values) if (!s.IsTerminal && !HasEscape(s)) throw new InvalidOperationException( $"BrainBuilder: 状态 '{s.Name}' 不是终态,却没有任何指向其他状态的出边。" + "进入后会永久停在这里,且全程零报错。请给它挂出边;" + "若「进去就不出来」本就是设计意图(如死亡态)," + "请用 AiStateFragments.Terminal() 声明,或对已有 builder 调 .Terminal() 标记;" + "若这是隔离测试单个模块的部分图,请改用 BuildPartial()。"); } return new AiGraph(_entry, _states, _globals); } /// /// 该状态是否有真正的出口。两点刻意的判定: /// 1. 自转换不算——AiRuntime.Switch 对 Target == 当前态直接 return false,出不去; /// 2. 全局转换不算——全局事件边要外部推信号才触发,不是自主出口。把它算作出口, /// 等于让「只有死了才出得去」的死角通过校验,而那正是本校验要暴露的东西。 /// static bool HasEscape(AiState s) { var ts = s.Transitions; for (int i = 0; i < ts.Count; i++) if (ts[i].Target != s.Name) return true; return false; } public sealed class StateBuilder { readonly AiState _state; internal StateBuilder(AiState state) { _state = state; } public StateBuilder OnEnter(Action fn) { _state.OnEnter = fn; return this; } public StateBuilder Tick(Action fn) { _state.OnTick = (c, _) => fn(c); return this; } public StateBuilder Tick(Action fn) { _state.OnTick = fn; return this; } public StateBuilder OnExit(Action fn) { _state.OnExit = fn; return this; } /// /// 标记为终态:声明"进入后不再离开"是设计意图。 /// Build() 会对非终态且无出口的态抛异常,本标记是唯一的豁免方式—— /// 用它而不是随手挂一条永假的边,意图才留在代码里。 /// public StateBuilder Terminal() { _state.IsTerminal = true; return this; } public TransitionBuilder To(string target) => new TransitionBuilder(this, _state, target); } public sealed class TransitionBuilder { readonly StateBuilder _stateBuilder; readonly AiState _state; readonly string _target; internal TransitionBuilder(StateBuilder sb, AiState state, string target) { _stateBuilder = sb; _state = state; _target = target; } /// /// 条件转换。label 为可读标签(用于 Mermaid 边 / trace 触发原因), /// C# 9 环境下需显式传入;不传则回退为 "cond"。 /// public StateBuilder When(Func cond, string label = null) { _state.AddTransition(Transition.OnCondition(_target, cond, label ?? "cond")); return _stateBuilder; } /// 事件转换。 public StateBuilder OnEvent(AiSignal evt) { _state.AddTransition(Transition.OnEvent(_target, evt)); return _stateBuilder; } /// 在本状态停留 seconds 秒后转换(读 runtime 的状态计时器)。 public StateBuilder After(float seconds) { _state.AddTransition(Transition.OnCondition( _target, c => AiRuntime.TimeInState(c) >= seconds, $"after {seconds}s")); return _stateBuilder; } } public sealed class GlobalBuilder { readonly BrainBuilder _owner; string _pendingTarget; internal GlobalBuilder(BrainBuilder owner) { _owner = owner; } public GlobalBuilder To(string target) { _pendingTarget = target; return this; } public GlobalBuilder OnEvent(AiSignal evt) { if (_pendingTarget == null) throw new System.InvalidOperationException("Global(): 请先调用 To(target) 再 OnEvent/When。"); _owner.AddGlobal(Transition.OnEvent(_pendingTarget, evt)); return this; } public GlobalBuilder When(Func cond, string label = null) { if (_pendingTarget == null) throw new System.InvalidOperationException("Global(): 请先调用 To(target) 再 OnEvent/When。"); _owner.AddGlobal(Transition.OnCondition(_pendingTarget, cond, label ?? "cond")); return this; } } } }