From c4e29b1f4fe84cd194b0be12c1c44dce69d443de Mon Sep 17 00:00:00 2001 From: milimoe Date: Tue, 3 Feb 2026 01:37:44 +0800 Subject: [PATCH] =?UTF-8?q?AIController=20=E4=BD=BF=E7=94=A8=E5=A4=9A?= =?UTF-8?q?=E7=BA=BF=E7=A8=8B=E8=AE=A1=E7=AE=97=EF=BC=9B=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E7=9B=AE=E6=A0=87=E5=89=8D=E7=9A=84=E8=AF=A2=E9=97=AE=E9=A1=BA?= =?UTF-8?q?=E5=BA=8F=E6=94=B9=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Controller/AIController.cs | 582 ++++++++++++++++++++--------------- Entity/Item/Item.cs | 16 +- Entity/Skill/NormalAttack.cs | 32 +- Entity/Skill/Skill.cs | 16 +- Model/GamingQueue.cs | 99 +++--- 5 files changed, 442 insertions(+), 303 deletions(-) diff --git a/Controller/AIController.cs b/Controller/AIController.cs index 266b659..c58000c 100644 --- a/Controller/AIController.cs +++ b/Controller/AIController.cs @@ -1,4 +1,5 @@ -using Milimoe.FunGame.Core.Entity; +using System.Collections.Concurrent; +using Milimoe.FunGame.Core.Entity; using Milimoe.FunGame.Core.Interface.Entity; using Milimoe.FunGame.Core.Library.Common.Addon; using Milimoe.FunGame.Core.Library.Constant; @@ -11,8 +12,19 @@ namespace Milimoe.FunGame.Core.Controller private readonly GamingQueue _queue = queue; private readonly GameMap _map = map; + public delegate double EvaluateSkillDelegate(Character character, Skill skill, List targets, double cost); + public delegate double EvaluateNormalAttackDelegate(Character character, NormalAttack normalAttack, List targets); + public delegate double EvaluateNonDirectionalSkillDelegate(Character character, Skill skill, Grid moveGrid, List castableGrids, List allEnemys, List allTeammates, double cost); + public delegate double EvaluateItemDelegate(Character character, Item item, List targets, double cost); + public delegate double CalculateTargetValueDelegate(Character character, ISkill skill); + public static event EvaluateSkillDelegate? EvaluateSkillEvent; + public static event EvaluateNormalAttackDelegate? EvaluateNormalAttackEvent; + public static event EvaluateNonDirectionalSkillDelegate? EvaluateNonDirectionalSkillEvent; + public static event EvaluateItemDelegate? EvaluateItemEvent; + public static event CalculateTargetValueDelegate? CalculateTargetValueEvent; + /// - /// AI的核心决策方法,根据当前游戏状态为角色选择最佳行动 + /// 核心决策方法:外部同步,内部异步并行计算所有独立决策的分数 /// /// 当前行动的AI角色 /// 角色的决策点 @@ -32,6 +44,10 @@ namespace Milimoe.FunGame.Core.Controller List availableSkills, List availableItems, List allEnemysInGame, List allTeammatesInGame, List selectableEnemys, List selectableTeammates, double pUseItem, double pCastSkill, double pNormalAttack) { + // 控制最大并发数 + int maxConcurrency = Math.Max(1, Environment.ProcessorCount / 2); + SemaphoreSlim semaphore = new(maxConcurrency); + // 动态调整概率 double dynamicPUseItem = pUseItem; double dynamicPCastSkill = pCastSkill; @@ -57,214 +73,306 @@ namespace Milimoe.FunGame.Core.Controller }; // 候选决策 - List candidateDecisions = []; + ConcurrentBag candidateDecisions = []; - // 遍历所有可能的移动目标格子 (包括起始格子本身) + // 封装单个移动格子的决策计算逻辑为异步任务 + List decisionTasks = []; foreach (Grid potentialMoveGrid in allPossibleMoveGrids) { - // 计算移动到这个格子的代价(曼哈顿距离) - int moveDistance = GameMap.CalculateManhattanDistance(startGrid, potentialMoveGrid); - double movePenalty = moveDistance * 0.5; // 每移动一步扣0.5分 - - if (pNormalAttack > 0) + // 捕获循环变量(避免闭包陷阱) + Grid currentMoveGrid = potentialMoveGrid; + Task task = Task.Run(async () => { - if (CanCharacterNormalAttack(character, dp)) + await semaphore.WaitAsync(); + try { - // 计算普通攻击的可达格子 - List normalAttackReachableGrids = _map.GetGridsByRange(potentialMoveGrid, character.ATR, true); - - List normalAttackReachableEnemys = [.. allEnemysInGame.Where(c => normalAttackReachableGrids.SelectMany(g => g.Characters).Contains(c) && !c.IsUnselectable && selectableEnemys.Contains(c)).Distinct()]; - List normalAttackReachableTeammates = [.. allTeammatesInGame.Where(c => normalAttackReachableGrids.SelectMany(g => g.Characters).Contains(c) && selectableTeammates.Contains(c)).Distinct()]; - - if (normalAttackReachableEnemys.Count > 0) - { - // 将筛选后的目标列表传递给 SelectTargets - List targets = SelectTargets(character, character.NormalAttack, normalAttackReachableEnemys, normalAttackReachableTeammates); - if (targets.Count > 0) - { - double currentScore = EvaluateNormalAttack(character, targets) - movePenalty; - double probabilityWeight = 1.0 + (normalizedPNormalAttack * 0.3); - candidateDecisions.Add(new AIDecision - { - ActionType = CharacterActionType.NormalAttack, - TargetMoveGrid = potentialMoveGrid, - SkillToUse = character.NormalAttack, - Targets = targets, - Score = currentScore, - ProbabilityWeight = probabilityWeight - }); - } - } + CalculateDecisionForGrid( + character, dp, startGrid, currentMoveGrid, + availableSkills, availableItems, allEnemysInGame, allTeammatesInGame, + selectableEnemys, selectableTeammates, + normalizedPUseItem, normalizedPCastSkill, normalizedPNormalAttack, + preferredAction, candidateDecisions + ); } - } - - if (pCastSkill > 0) - { - foreach (Skill skill in availableSkills) + catch (Exception ex) { - if (CanCharacterUseSkill(character, skill, dp) && _queue.CheckCanCast(character, skill, out double cost)) - { - // 计算当前技能的可达格子 - List skillReachableGrids = _map.GetGridsByRange(potentialMoveGrid, skill.CastRange, true); - - if (skill.IsNonDirectional) - { - AIDecision? nonDirDecision = EvaluateNonDirectionalSkill(character, skill, potentialMoveGrid, skillReachableGrids, allEnemysInGame, allTeammatesInGame, cost); - - if (nonDirDecision != null && nonDirDecision.Score > bestDecision.Score) - { - bestDecision = nonDirDecision; - } - } - else - { - List skillReachableEnemys = [.. allEnemysInGame.Where(c => skillReachableGrids.SelectMany(g => g.Characters).Contains(c) && !c.IsUnselectable && selectableEnemys.Contains(c)).Distinct()]; - List skillReachableTeammates = [.. allTeammatesInGame.Where(c => skillReachableGrids.SelectMany(g => g.Characters).Contains(c) && selectableTeammates.Contains(c)).Distinct()]; - - // 检查是否有可用的目标(敌人或队友,取决于技能类型) - if (skillReachableEnemys.Count > 0 || skillReachableTeammates.Count > 0) - { - // 将筛选后的目标列表传递给 SelectTargets - List targets = SelectTargets(character, skill, skillReachableEnemys, skillReachableTeammates); - if (targets.Count > 0) - { - double currentScore = EvaluateSkill(character, skill, targets, cost) - movePenalty; - double probabilityWeight = 1.0 + (normalizedPCastSkill * 0.3); - candidateDecisions.Add(new AIDecision - { - ActionType = CharacterActionType.PreCastSkill, - TargetMoveGrid = potentialMoveGrid, - SkillToUse = skill, - Targets = targets, - Score = currentScore, - ProbabilityWeight = probabilityWeight - }); - } - } - } - } + // 单个格子的计算异常不影响整体,记录日志 + Console.WriteLine($"计算格子[{currentMoveGrid.X},{currentMoveGrid.Y}]决策失败:{ex.Message}"); } - } - - if (pUseItem > 0) - { - foreach (Item item in availableItems) + finally { - if (item.Skills.Active != null && CanCharacterUseItem(character, item, dp) && _queue.CheckCanCast(character, item.Skills.Active, out double cost)) - { - Skill itemSkill = item.Skills.Active; - - // 计算当前物品技能的可达格子 - List itemSkillReachableGrids = _map.GetGridsByRange(potentialMoveGrid, itemSkill.CastRange, true); - - if (itemSkill.IsNonDirectional) - { - AIDecision? nonDirDecision = EvaluateNonDirectionalSkill(character, itemSkill, potentialMoveGrid, itemSkillReachableGrids, allEnemysInGame, allTeammatesInGame, cost); - - if (nonDirDecision != null && nonDirDecision.Score > bestDecision.Score) - { - bestDecision = nonDirDecision; - } - } - else - { - List itemSkillReachableEnemys = [.. allEnemysInGame.Where(c => itemSkillReachableGrids.SelectMany(g => g.Characters).Contains(c) && !c.IsUnselectable && selectableEnemys.Contains(c)).Distinct()]; - List itemSkillReachableTeammates = [.. allTeammatesInGame.Where(c => itemSkillReachableGrids.SelectMany(g => g.Characters).Contains(c) && selectableTeammates.Contains(c)).Distinct()]; - - // 检查是否有可用的目标 - if (itemSkillReachableEnemys.Count > 0 || itemSkillReachableTeammates.Count > 0) - { - // 将筛选后的目标列表传递给 SelectTargets - List targetsForItem = SelectTargets(character, itemSkill, itemSkillReachableEnemys, itemSkillReachableTeammates); - if (targetsForItem.Count > 0) - { - double currentScore = EvaluateItem(character, item, targetsForItem, cost) - movePenalty; - double probabilityWeight = 1.0 + (normalizedPUseItem * 0.3); - candidateDecisions.Add(new AIDecision - { - ActionType = CharacterActionType.UseItem, - TargetMoveGrid = potentialMoveGrid, - ItemToUse = item, - SkillToUse = itemSkill, - Targets = targetsForItem, - Score = currentScore, - ProbabilityWeight = probabilityWeight - }); - } - } - } - } + semaphore.Release(); } - } + }); + decisionTasks.Add(task); + } - // 如果从该格子没有更好的行动,但移动本身有价值 - // 只有当当前最佳决策是“结束回合”或分数很低时,才考虑纯粹的移动 - if (potentialMoveGrid != startGrid && bestDecision.Score < 0) // 如果当前最佳决策是负分(即什么都不做) - { - double pureMoveScore = -movePenalty; // 移动本身有代价 + // 等待所有异步任务完成 + Task.WaitAll(decisionTasks); - // 为纯粹移动逻辑重新计算综合可达敌人列表 - List tempAttackGridsForPureMove = _map.GetGridsByRange(potentialMoveGrid, character.ATR, true); - List tempCastGridsForPureMove = []; - foreach (Skill skill in availableSkills) - { - tempCastGridsForPureMove.AddRange(_map.GetGridsByRange(potentialMoveGrid, skill.CastRange, true)); - } - foreach (Item item in availableItems) - { - if (item.Skills.Active != null) - { - tempCastGridsForPureMove.AddRange(_map.GetGridsByRange(potentialMoveGrid, item.Skills.Active.CastRange, true)); - } - } - List tempAllReachableGridsForPureMove = [.. tempAttackGridsForPureMove.Union(tempCastGridsForPureMove).Distinct()]; - List tempCurrentReachableEnemysForPureMove = [.. allEnemysInGame.Where(c => tempAllReachableGridsForPureMove.SelectMany(g => g.Characters).Contains(c) && !c.IsUnselectable && selectableEnemys.Contains(c)).Distinct()]; - - // 如果当前位置无法攻击任何敌人,但地图上还有敌人,尝试向最近的敌人移动 - if (tempCurrentReachableEnemysForPureMove.Count == 0 && allEnemysInGame.Count > 0) // 使用新计算的列表 - { - Character? target = allEnemysInGame.OrderBy(e => GameMap.CalculateManhattanDistance(potentialMoveGrid, _map.GetCharacterCurrentGrid(e) ?? Grid.Empty)).FirstOrDefault(); - - if (target != null) - { - Grid? nearestEnemyGrid = _map.GetCharacterCurrentGrid(target); - if (nearestEnemyGrid != null) - { - // 奖励靠近敌人 - pureMoveScore += (10 - GameMap.CalculateManhattanDistance(potentialMoveGrid, nearestEnemyGrid)) * 0.1; - } - } - } - - candidateDecisions.Add(new AIDecision - { - ActionType = CharacterActionType.Move, - TargetMoveGrid = potentialMoveGrid, - Targets = [], - Score = pureMoveScore, - IsPureMove = true - }); - } - - // 偏好类型额外加分 - if (preferredAction.HasValue) - { - foreach (var decision in candidateDecisions) - { - if (decision.ActionType == preferredAction.Value) - { - decision.Score *= 1.2; - } - } - } - - // 最终决策 - bestDecision = candidateDecisions.OrderByDescending(d => d.Score * d.ProbabilityWeight).FirstOrDefault() ?? bestDecision; + // 从所有候选决策中选出最高分的(保留原有评分权重逻辑) + if (!candidateDecisions.IsEmpty) + { + bestDecision = candidateDecisions + .OrderByDescending(d => d.Score * d.ProbabilityWeight) + .FirstOrDefault() ?? bestDecision; } return bestDecision; } + /// + /// 异步执行的核心:计算单个移动格子的所有可能决策,并添加到线程安全容器 + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + private void CalculateDecisionForGrid( + Character character, DecisionPoints dp, Grid startGrid, Grid potentialMoveGrid, + List availableSkills, List availableItems, List allEnemysInGame, List allTeammatesInGame, + List selectableEnemys, List selectableTeammates, + double normalizedPUseItem, double normalizedPCastSkill, double normalizedPNormalAttack, + CharacterActionType? preferredAction, + ConcurrentBag candidateDecisions) + { + // 计算移动惩罚 + int moveDistance = GameMap.CalculateManhattanDistance(startGrid, potentialMoveGrid); + double movePenalty = moveDistance * 0.5; + + // 计算普通攻击决策 + if (normalizedPNormalAttack > 0 && CanCharacterNormalAttack(character, dp)) + { + List normalAttackReachableGrids = _map.GetGridsByRange(potentialMoveGrid, character.ATR, true); + List normalAttackReachableEnemys = [.. allEnemysInGame.Where(c => + normalAttackReachableGrids.SelectMany(g => g.Characters).Contains(c) + && !c.IsUnselectable + && selectableEnemys.Contains(c)).Distinct()]; + + if (normalAttackReachableEnemys.Count > 0) + { + List targets = SelectTargets(character, character.NormalAttack, normalAttackReachableEnemys, []); + if (targets.Count > 0) + { + double currentScore = EvaluateNormalAttack(character, targets) - movePenalty; + double probabilityWeight = 1.0 + (normalizedPNormalAttack * 0.3); + AIDecision attackDecision = new() + { + ActionType = CharacterActionType.NormalAttack, + TargetMoveGrid = potentialMoveGrid, + SkillToUse = character.NormalAttack, + Targets = targets, + Score = currentScore, + ProbabilityWeight = probabilityWeight + }; + // 偏好类型加分 + if (preferredAction.HasValue && attackDecision.ActionType == preferredAction.Value) + { + attackDecision.Score *= 1.2; + } + candidateDecisions.Add(attackDecision); + } + } + } + + // 计算技能释放决策 + if (normalizedPCastSkill > 0) + { + foreach (Skill skill in availableSkills) + { + if (CanCharacterUseSkill(character, skill, dp) && _queue.CheckCanCast(character, skill, out double cost)) + { + List skillReachableGrids = _map.GetGridsByRange(potentialMoveGrid, skill.CastRange, true); + + if (skill.IsNonDirectional) + { + AIDecision? nonDirDecision = EvaluateNonDirectionalSkill( + character, skill, potentialMoveGrid, skillReachableGrids, + allEnemysInGame, allTeammatesInGame, cost); + + if (nonDirDecision != null) + { + // 偏好类型加分 + if (preferredAction.HasValue && nonDirDecision.ActionType == preferredAction.Value) + { + nonDirDecision.Score *= 1.2; + } + candidateDecisions.Add(nonDirDecision); + } + } + else + { + List skillReachableEnemys = [.. allEnemysInGame.Where(c => + skillReachableGrids.SelectMany(g => g.Characters).Contains(c) + && !c.IsUnselectable + && selectableEnemys.Contains(c)).Distinct()]; + List skillReachableTeammates = [.. allTeammatesInGame.Where(c => + skillReachableGrids.SelectMany(g => g.Characters).Contains(c) + && selectableTeammates.Contains(c)).Distinct()]; + + if (skillReachableEnemys.Count > 0 || skillReachableTeammates.Count > 0) + { + List targets = SelectTargets(character, skill, skillReachableEnemys, skillReachableTeammates); + if (targets.Count > 0) + { + double currentScore = EvaluateSkill(character, skill, targets, cost) - movePenalty; + double probabilityWeight = 1.0 + (normalizedPCastSkill * 0.3); + AIDecision skillDecision = new() + { + ActionType = CharacterActionType.PreCastSkill, + TargetMoveGrid = potentialMoveGrid, + SkillToUse = skill, + Targets = targets, + Score = currentScore, + ProbabilityWeight = probabilityWeight + }; + // 偏好类型加分 + if (preferredAction.HasValue && skillDecision.ActionType == preferredAction.Value) + { + skillDecision.Score *= 1.2; + } + candidateDecisions.Add(skillDecision); + } + } + } + } + } + } + + // 计算物品使用决策 + if (normalizedPUseItem > 0) + { + foreach (Item item in availableItems) + { + if (item.Skills.Active != null && CanCharacterUseItem(character, item, dp) && _queue.CheckCanCast(character, item.Skills.Active, out double cost)) + { + Skill itemSkill = item.Skills.Active; + List itemSkillReachableGrids = _map.GetGridsByRange(potentialMoveGrid, itemSkill.CastRange, true); + + if (itemSkill.IsNonDirectional) + { + AIDecision? nonDirDecision = EvaluateNonDirectionalSkill( + character, itemSkill, potentialMoveGrid, itemSkillReachableGrids, + allEnemysInGame, allTeammatesInGame, cost); + + if (nonDirDecision != null) + { + // 偏好类型加分 + if (preferredAction.HasValue && nonDirDecision.ActionType == preferredAction.Value) + { + nonDirDecision.Score *= 1.2; + } + candidateDecisions.Add(nonDirDecision); + } + } + else + { + List itemSkillReachableEnemys = [.. allEnemysInGame.Where(c => + itemSkillReachableGrids.SelectMany(g => g.Characters).Contains(c) + && !c.IsUnselectable + && selectableEnemys.Contains(c)).Distinct()]; + List itemSkillReachableTeammates = [.. allTeammatesInGame.Where(c => + itemSkillReachableGrids.SelectMany(g => g.Characters).Contains(c) + && selectableTeammates.Contains(c)).Distinct()]; + + if (itemSkillReachableEnemys.Count > 0 || itemSkillReachableTeammates.Count > 0) + { + List targetsForItem = SelectTargets(character, itemSkill, itemSkillReachableEnemys, itemSkillReachableTeammates); + if (targetsForItem.Count > 0) + { + double currentScore = EvaluateItem(character, item, targetsForItem, cost) - movePenalty; + double probabilityWeight = 1.0 + (normalizedPUseItem * 0.3); + AIDecision itemDecision = new() + { + ActionType = CharacterActionType.UseItem, + TargetMoveGrid = potentialMoveGrid, + ItemToUse = item, + SkillToUse = itemSkill, + Targets = targetsForItem, + Score = currentScore, + ProbabilityWeight = probabilityWeight + }; + // 偏好类型加分 + if (preferredAction.HasValue && itemDecision.ActionType == preferredAction.Value) + { + itemDecision.Score *= 1.2; + } + candidateDecisions.Add(itemDecision); + } + } + } + } + } + } + + // 计算纯移动决策 + if (potentialMoveGrid != startGrid) + { + double pureMoveScore = -movePenalty; + + List tempAttackGridsForPureMove = _map.GetGridsByRange(potentialMoveGrid, character.ATR, true); + List tempCastGridsForPureMove = []; + foreach (Skill skill in availableSkills) + { + tempCastGridsForPureMove.AddRange(_map.GetGridsByRange(potentialMoveGrid, skill.CastRange, true)); + } + foreach (Item item in availableItems) + { + if (item.Skills.Active != null) + { + tempCastGridsForPureMove.AddRange(_map.GetGridsByRange(potentialMoveGrid, item.Skills.Active.CastRange, true)); + } + } + List tempAllReachableGridsForPureMove = [.. tempAttackGridsForPureMove.Union(tempCastGridsForPureMove).Distinct()]; + List tempCurrentReachableEnemysForPureMove = [.. allEnemysInGame.Where(c => + tempAllReachableGridsForPureMove.SelectMany(g => g.Characters).Contains(c) + && !c.IsUnselectable + && selectableEnemys.Contains(c)).Distinct()]; + + if (tempCurrentReachableEnemysForPureMove.Count == 0 && allEnemysInGame.Count > 0) + { + Character? target = allEnemysInGame.OrderBy(e => + GameMap.CalculateManhattanDistance(potentialMoveGrid, _map.GetCharacterCurrentGrid(e) ?? Grid.Empty)).FirstOrDefault(); + + if (target != null) + { + Grid? nearestEnemyGrid = _map.GetCharacterCurrentGrid(target); + if (nearestEnemyGrid != null) + { + pureMoveScore += (10 - GameMap.CalculateManhattanDistance(potentialMoveGrid, nearestEnemyGrid)) * 0.1; + } + } + } + + AIDecision moveDecision = new() + { + ActionType = CharacterActionType.Move, + TargetMoveGrid = potentialMoveGrid, + Targets = [], + Score = pureMoveScore, + IsPureMove = true, + ProbabilityWeight = 1.0 // 纯移动无概率权重 + }; + // 偏好类型加分(如果移动是偏好类型) + if (preferredAction.HasValue && moveDecision.ActionType == preferredAction.Value) + { + moveDecision.Score *= 1.2; + } + candidateDecisions.Add(moveDecision); + } + } + // --- AI 决策辅助方法 --- // 获取偏好行动类型 @@ -343,42 +451,39 @@ namespace Milimoe.FunGame.Core.Controller // 检查角色是否能进行普通攻击(基于状态) private static bool CanCharacterNormalAttack(Character character, DecisionPoints dp) { - return dp.CheckActionTypeQuota(CharacterActionType.NormalAttack) && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostNormalAttack && - character.CharacterState != CharacterState.NotActionable && - character.CharacterState != CharacterState.ActionRestricted && - character.CharacterState != CharacterState.BattleRestricted && - character.CharacterState != CharacterState.AttackRestricted; + return dp.CheckActionTypeQuota(CharacterActionType.NormalAttack) + && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostNormalAttack + && character.CharacterState != CharacterState.NotActionable + && character.CharacterState != CharacterState.ActionRestricted + && character.CharacterState != CharacterState.BattleRestricted + && character.CharacterState != CharacterState.AttackRestricted; } // 检查角色是否能使用某个技能(基于状态) private static bool CanCharacterUseSkill(Character character, Skill skill, DecisionPoints dp) { - return ((skill.SkillType == SkillType.Magic && dp.CheckActionTypeQuota(CharacterActionType.PreCastSkill) && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostMagic) || - (skill.SkillType == SkillType.Skill && dp.CheckActionTypeQuota(CharacterActionType.CastSkill) && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostSkill) || - (skill.SkillType == SkillType.SuperSkill && dp.CheckActionTypeQuota(CharacterActionType.CastSuperSkill)) && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostSuperSkill) && - character.CharacterState != CharacterState.NotActionable && - character.CharacterState != CharacterState.ActionRestricted && - character.CharacterState != CharacterState.BattleRestricted && - character.CharacterState != CharacterState.SkillRestricted; + return ( + (skill.SkillType == SkillType.Magic && dp.CheckActionTypeQuota(CharacterActionType.PreCastSkill) && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostMagic) + || (skill.SkillType == SkillType.Skill && dp.CheckActionTypeQuota(CharacterActionType.CastSkill) && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostSkill) + || (skill.SkillType == SkillType.SuperSkill && dp.CheckActionTypeQuota(CharacterActionType.CastSuperSkill) && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostSuperSkill) + ) + && character.CharacterState != CharacterState.NotActionable + && character.CharacterState != CharacterState.ActionRestricted + && character.CharacterState != CharacterState.BattleRestricted + && character.CharacterState != CharacterState.SkillRestricted; } // 检查角色是否能使用某个物品(基于状态) private static bool CanCharacterUseItem(Character character, Item item, DecisionPoints dp) { - return dp.CheckActionTypeQuota(CharacterActionType.UseItem) && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostItem && - character.CharacterState != CharacterState.NotActionable && - (character.CharacterState != CharacterState.ActionRestricted || item.ItemType == ItemType.Consumable) && // 行动受限只能用消耗品 - character.CharacterState != CharacterState.BattleRestricted; + return dp.CheckActionTypeQuota(CharacterActionType.UseItem) + && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostItem + && character.CharacterState != CharacterState.NotActionable + && (character.CharacterState != CharacterState.ActionRestricted || item.ItemType == ItemType.Consumable) + && character.CharacterState != CharacterState.BattleRestricted; } - /// - /// 选择技能的最佳目标 - /// - /// - /// - /// - /// - /// + // 选择技能的最佳目标 private static List SelectTargets(Character character, ISkill skill, List enemys, List teammates) { List targets = skill.GetSelectableTargets(character, enemys, teammates); @@ -386,12 +491,7 @@ namespace Milimoe.FunGame.Core.Controller return [.. targets.OrderBy(o => Random.Shared.Next()).Take(count)]; } - /// - /// 评估普通攻击的价值 - /// - /// - /// - /// + // 评估普通攻击的价值 private static double EvaluateNormalAttack(Character character, List targets) { double score = 0; @@ -401,24 +501,16 @@ namespace Milimoe.FunGame.Core.Controller score += damage; if (target.HP <= damage) score += 100; } + score += EvaluateNormalAttackEvent?.Invoke(character, character.NormalAttack, targets) ?? 0; return score; } - /// - /// 评估技能的价值 - /// - /// - /// - /// - /// - /// + // 评估技能的价值 private static double EvaluateSkill(Character character, Skill skill, List targets, double cost) { double score = 0; score += targets.Sum(t => CalculateTargetValue(t, skill)); - //score -= cost * 5; - //score -= skill.RealCD * 2; - //score -= skill.HardnessTime * 2; + score += EvaluateSkillEvent?.Invoke(character, skill, targets, cost) ?? 0; return score; } @@ -455,6 +547,7 @@ namespace Milimoe.FunGame.Core.Controller double movePenalty = GameMap.CalculateManhattanDistance(_map.GetCharacterCurrentGrid(character)!, moveGrid) * 0.5; double finalScore = bestSkillScore - movePenalty; + finalScore += EvaluateNonDirectionalSkillEvent?.Invoke(character, skill, moveGrid, castableGrids, allEnemys, allTeammates, cost) ?? 0; return new AIDecision { @@ -463,33 +556,24 @@ namespace Milimoe.FunGame.Core.Controller SkillToUse = skill, Targets = [], TargetGrids = bestTargetGrids, - Score = finalScore + Score = finalScore, + ProbabilityWeight = 1.0 // 非指向性技能默认权重 }; } - /// - /// 评估物品的价值 - /// - /// - /// - /// - /// - /// + // 评估物品的价值 private static double EvaluateItem(Character character, Item item, List targets, double cost) { double score = Random.Shared.Next(1000); + score += EvaluateItemEvent?.Invoke(character, item, targets, cost) ?? 0; return score; } - /// - /// 辅助函数:计算单个目标在某个技能下的价值 - /// - /// - /// - /// + // 辅助函数:计算单个目标在某个技能下的价值 private static double CalculateTargetValue(Character target, ISkill skill) { double value = Random.Shared.Next(1000); + value += CalculateTargetValueEvent?.Invoke(target, skill) ?? 0; return value; } } diff --git a/Entity/Item/Item.cs b/Entity/Item/Item.cs index 4dcd425..2629157 100644 --- a/Entity/Item/Item.cs +++ b/Entity/Item/Item.cs @@ -306,13 +306,25 @@ namespace Milimoe.FunGame.Core.Entity /// 在选取目标前向角色(玩家)发起询问 /// /// - /// + /// /// - public virtual InquiryOptions? InquiryBeforeTargetSelection(Character character, Item item) + public virtual InquiryOptions? InquiryBeforeTargetSelection(Character character, DecisionPoints dp) { return null; } + /// + /// 处理在选取目标前发起的询问的结果 + /// + /// + /// + /// + /// + public virtual void ResolveInquiryBeforeTargetSelection(Character character, DecisionPoints dp, InquiryOptions options, InquiryResponse response) + { + + } + /// /// 局内使用物品触发 /// diff --git a/Entity/Skill/NormalAttack.cs b/Entity/Skill/NormalAttack.cs index 6dcf101..fd306fa 100644 --- a/Entity/Skill/NormalAttack.cs +++ b/Entity/Skill/NormalAttack.cs @@ -483,19 +483,43 @@ namespace Milimoe.FunGame.Core.Entity /// /// /// + /// /// - public delegate InquiryOptions? NormalAttackInquiryOptionsDelegate(Character character, NormalAttack normalAttack); + public delegate InquiryOptions? NormalAttackInquiryOptionsDelegate(Character character, NormalAttack normalAttack, DecisionPoints dp); public event NormalAttackInquiryOptionsDelegate? InquiryBeforeTargetSelectionEvent; /// /// 触发选择目标前的询问事件 /// /// - /// - public InquiryOptions? OnInquiryBeforeTargetSelection(Character character, NormalAttack normalAttack) + /// + public InquiryOptions? OnInquiryBeforeTargetSelection(Character character, DecisionPoints dp) { - return InquiryBeforeTargetSelectionEvent?.Invoke(character, normalAttack); + return InquiryBeforeTargetSelectionEvent?.Invoke(character, this, dp); } + /// + /// 处理在选取目标前发起询问的结果事件 + /// + /// + /// + /// + /// + /// + public delegate void ResolveInquiryBeforeTargetSelection(Character character, NormalAttack normalAttack, DecisionPoints dp, InquiryOptions options, InquiryResponse response); + public event ResolveInquiryBeforeTargetSelection? ResolveInquiryBeforeTargetSelectionEvent; + /// + /// 触发处理选取目标前发起询问的结果事件 + /// + /// + /// + /// + /// + public void OnResolveInquiryBeforeTargetSelection(Character character, DecisionPoints dp, InquiryOptions options, InquiryResponse response) + { + ResolveInquiryBeforeTargetSelectionEvent?.Invoke(character, this, dp, options, response); + } + + /// /// 等级 /// diff --git a/Entity/Skill/Skill.cs b/Entity/Skill/Skill.cs index cb38b1d..86145f2 100644 --- a/Entity/Skill/Skill.cs +++ b/Entity/Skill/Skill.cs @@ -458,13 +458,25 @@ namespace Milimoe.FunGame.Core.Entity /// 在选取目标前向角色(玩家)发起询问 /// /// - /// + /// /// - public virtual InquiryOptions? InquiryBeforeTargetSelection(Character character, Skill skill) + public virtual InquiryOptions? InquiryBeforeTargetSelection(Character character, DecisionPoints dp) { return null; } + /// + /// 处理在选取目标前发起的询问的结果 + /// + /// + /// + /// + /// + public virtual void ResolveInquiryBeforeTargetSelection(Character character, DecisionPoints dp, InquiryOptions options, InquiryResponse response) + { + + } + /// /// 获取可选择的目标列表 /// diff --git a/Model/GamingQueue.cs b/Model/GamingQueue.cs index 6154036..ec49496 100644 --- a/Model/GamingQueue.cs +++ b/Model/GamingQueue.cs @@ -1310,8 +1310,6 @@ namespace Milimoe.FunGame.Core.Model moved = CharacterMove(character, dp, aiDecision.TargetMoveGrid, startGrid); } - int costDP = dp.GetActionPointCost(type); - effects = [.. character.Effects.Where(e => e.IsInEffect).OrderByDescending(e => e.Priority)]; foreach (Effect effect in effects) { @@ -1346,6 +1344,16 @@ namespace Milimoe.FunGame.Core.Model } else if (type == CharacterActionType.NormalAttack) { + // 使用普通攻击逻辑 + // 如果有询问,先进行询问 + character.NormalAttack.GamingQueue = this; + if (character.NormalAttack.OnInquiryBeforeTargetSelection(character, dp) is InquiryOptions inquiry) + { + InquiryResponse response = Inquiry(character, inquiry); + character.NormalAttack.OnResolveInquiryBeforeTargetSelection(character, dp, inquiry, response); + } + + int costDP = dp.GetActionPointCost(type); if (!forceAction && (character.CharacterState == CharacterState.NotActionable || character.CharacterState == CharacterState.ActionRestricted || character.CharacterState == CharacterState.BattleRestricted || @@ -1363,13 +1371,6 @@ namespace Milimoe.FunGame.Core.Model } else { - // 使用普通攻击逻辑 - // 如果有询问,先进行询问 - character.NormalAttack.GamingQueue = this; - if (character.NormalAttack.OnInquiryBeforeTargetSelection(character, character.NormalAttack) is InquiryOptions inquiry) - { - Inquiry(character, inquiry); - } // 选择目标 List targets; if (aiDecision != null) @@ -1446,21 +1447,22 @@ namespace Milimoe.FunGame.Core.Model skill.GamingQueue = this; List targets = []; List grids = []; - costDP = dp.GetActionPointCost(type, skill); - if (dp.CurrentDecisionPoints < costDP) + if (skill.SkillType == SkillType.Magic) { - if (IsDebug) WriteLine($"[ {character} ] 想要释放 [ {skill.Name} ],但决策点不足,无法释放技能!"); - } - else if (skill.SkillType == SkillType.Magic) - { - if (CheckCanCast(character, skill, out double cost)) + // 如果有询问,先进行询问 + if (skill.InquiryBeforeTargetSelection(character, dp) is InquiryOptions inquiry) { - // 如果有询问,先进行询问 - if (skill.InquiryBeforeTargetSelection(character, skill) is InquiryOptions inquiry) - { - Inquiry(character, inquiry); - } + InquiryResponse response = Inquiry(character, inquiry); + skill.ResolveInquiryBeforeTargetSelection(character, dp, inquiry, response); + } + int costDP = dp.GetActionPointCost(type, skill); + if (dp.CurrentDecisionPoints < costDP) + { + if (IsDebug) WriteLine($"[ {character} ] 想要释放 [ {skill.Name} ],但决策点不足,无法释放技能!"); + } + else if (CheckCanCast(character, skill, out double cost)) + { // 吟唱前需要先选取目标 List castRange = []; if (_map != null && realGrid != null) @@ -1510,29 +1512,31 @@ namespace Milimoe.FunGame.Core.Model } } } - else if (skill is CourageCommandSkill && dp.CourageCommandSkill) - { - if (IsDebug) WriteLine($"[ {character} ] 想要释放 [ {skill.Name} ],但该回合已经使用过勇气指令,无法再次使用勇气指令!"); - } - else if (skill is not CourageCommandSkill && !skill.IsSuperSkill && !dp.CheckActionTypeQuota(CharacterActionType.CastSkill)) - { - if (IsDebug) WriteLine($"[ {character} ] 想要释放 [ {skill.Name} ],但该回合使用战技的次数已超过决策点配额,无法再次使用战技!"); - } - else if (skill is not CourageCommandSkill && skill.IsSuperSkill && !dp.CheckActionTypeQuota(CharacterActionType.CastSuperSkill)) - { - if (IsDebug) WriteLine($"[ {character} ] 想要释放 [ {skill.Name} ],但该回合使用爆发技的次数已超过决策点配额,无法再次使用爆发技!"); - } else { - // 只有魔法需要吟唱,战技和爆发技直接释放 - if (CheckCanCast(character, skill, out double cost)) + // 如果有询问,先进行询问 + if (skill.InquiryBeforeTargetSelection(character, dp) is InquiryOptions inquiry) { - // 如果有询问,先进行询问 - if (skill.InquiryBeforeTargetSelection(character, skill) is InquiryOptions inquiry) - { - Inquiry(character, inquiry); - } + InquiryResponse response = Inquiry(character, inquiry); + skill.ResolveInquiryBeforeTargetSelection(character, dp, inquiry, response); + } + int costDP = dp.GetActionPointCost(type, skill); + if (skill is CourageCommandSkill && dp.CourageCommandSkill) + { + if (IsDebug) WriteLine($"[ {character} ] 想要释放 [ {skill.Name} ],但该回合已经使用过勇气指令,无法再次使用勇气指令!"); + } + else if (skill is not CourageCommandSkill && !skill.IsSuperSkill && !dp.CheckActionTypeQuota(CharacterActionType.CastSkill)) + { + if (IsDebug) WriteLine($"[ {character} ] 想要释放 [ {skill.Name} ],但该回合使用战技的次数已超过决策点配额,无法再次使用战技!"); + } + else if (skill is not CourageCommandSkill && skill.IsSuperSkill && !dp.CheckActionTypeQuota(CharacterActionType.CastSuperSkill)) + { + if (IsDebug) WriteLine($"[ {character} ] 想要释放 [ {skill.Name} ],但该回合使用爆发技的次数已超过决策点配额,无法再次使用爆发技!"); + } + // 只有魔法需要吟唱,战技和爆发技直接释放 + else if (CheckCanCast(character, skill, out double cost)) + { List castRange = []; if (_map != null && realGrid != null) { @@ -1686,7 +1690,6 @@ namespace Milimoe.FunGame.Core.Model { WriteLine($"[ {character} ] 想要释放 [ {skill.Name} ],但是没有目标!"); } - _castingSkills.Remove(character); WriteLine($"[ {character} ] 放弃释放技能!"); character.CharacterState = CharacterState.Actionable; character.UpdateCharacterState(); @@ -1755,7 +1758,6 @@ namespace Milimoe.FunGame.Core.Model } else { - _castingSuperSkills.Remove(character); WriteLine($"[ {character} ] 因能量不足放弃释放爆发技!"); character.CharacterState = CharacterState.Actionable; character.UpdateCharacterState(); @@ -1787,14 +1789,17 @@ namespace Milimoe.FunGame.Core.Model Skill skill = item.Skills.Active; // 如果有询问,先进行询问 - if (item.InquiryBeforeTargetSelection(character, item) is InquiryOptions inquiry) + if (item.InquiryBeforeTargetSelection(character, dp) is InquiryOptions inquiry) { - Inquiry(character, inquiry); + InquiryResponse response = Inquiry(character, inquiry); + item.ResolveInquiryBeforeTargetSelection(character, dp, inquiry, response); } - if (skill.InquiryBeforeTargetSelection(character, skill) is InquiryOptions inquiry2) + // 如果有询问,先进行询问 + if (skill.InquiryBeforeTargetSelection(character, dp) is InquiryOptions inquiry2) { - Inquiry(character, inquiry2); + InquiryResponse response = Inquiry(character, inquiry2); + skill.ResolveInquiryBeforeTargetSelection(character, dp, inquiry2, response); } List castRange = []; @@ -1804,6 +1809,8 @@ namespace Milimoe.FunGame.Core.Model enemys = [.. enemys.Where(castRange.SelectMany(g => g.Characters).Contains)]; teammates = [.. teammates.Where(castRange.SelectMany(g => g.Characters).Contains)]; } + + int costDP = dp.GetActionPointCost(type); if (dp.CurrentDecisionPoints < costDP) { if (IsDebug) WriteLine($"[ {character} ] 想要使用物品 [ {item.Name} ],但决策点不足,无法使用物品!");