AIController 使用多线程计算;选择目标前的询问顺序改动

This commit is contained in:
milimoe 2026-02-03 01:37:44 +08:00
parent 4c01320b7a
commit c4e29b1f4f
Signed by: milimoe
GPG Key ID: 9554D37E4B8991D0
5 changed files with 442 additions and 303 deletions

View File

@ -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.Interface.Entity;
using Milimoe.FunGame.Core.Library.Common.Addon; using Milimoe.FunGame.Core.Library.Common.Addon;
using Milimoe.FunGame.Core.Library.Constant; using Milimoe.FunGame.Core.Library.Constant;
@ -11,8 +12,19 @@ namespace Milimoe.FunGame.Core.Controller
private readonly GamingQueue _queue = queue; private readonly GamingQueue _queue = queue;
private readonly GameMap _map = map; private readonly GameMap _map = map;
public delegate double EvaluateSkillDelegate(Character character, Skill skill, List<Character> targets, double cost);
public delegate double EvaluateNormalAttackDelegate(Character character, NormalAttack normalAttack, List<Character> targets);
public delegate double EvaluateNonDirectionalSkillDelegate(Character character, Skill skill, Grid moveGrid, List<Grid> castableGrids, List<Character> allEnemys, List<Character> allTeammates, double cost);
public delegate double EvaluateItemDelegate(Character character, Item item, List<Character> 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;
/// <summary> /// <summary>
/// AI的核心决策方法根据当前游戏状态为角色选择最佳行动 /// 核心决策方法:外部同步,内部异步并行计算所有独立决策的分数
/// </summary> /// </summary>
/// <param name="character">当前行动的AI角色</param> /// <param name="character">当前行动的AI角色</param>
/// <param name="dp">角色的决策点</param> /// <param name="dp">角色的决策点</param>
@ -32,6 +44,10 @@ namespace Milimoe.FunGame.Core.Controller
List<Skill> availableSkills, List<Item> availableItems, List<Character> allEnemysInGame, List<Character> allTeammatesInGame, List<Skill> availableSkills, List<Item> availableItems, List<Character> allEnemysInGame, List<Character> allTeammatesInGame,
List<Character> selectableEnemys, List<Character> selectableTeammates, double pUseItem, double pCastSkill, double pNormalAttack) List<Character> selectableEnemys, List<Character> selectableTeammates, double pUseItem, double pCastSkill, double pNormalAttack)
{ {
// 控制最大并发数
int maxConcurrency = Math.Max(1, Environment.ProcessorCount / 2);
SemaphoreSlim semaphore = new(maxConcurrency);
// 动态调整概率 // 动态调整概率
double dynamicPUseItem = pUseItem; double dynamicPUseItem = pUseItem;
double dynamicPCastSkill = pCastSkill; double dynamicPCastSkill = pCastSkill;
@ -57,214 +73,306 @@ namespace Milimoe.FunGame.Core.Controller
}; };
// 候选决策 // 候选决策
List<AIDecision> candidateDecisions = []; ConcurrentBag<AIDecision> candidateDecisions = [];
// 遍历所有可能的移动目标格子 (包括起始格子本身) // 封装单个移动格子的决策计算逻辑为异步任务
List<Task> decisionTasks = [];
foreach (Grid potentialMoveGrid in allPossibleMoveGrids) foreach (Grid potentialMoveGrid in allPossibleMoveGrids)
{ {
// 计算移动到这个格子的代价(曼哈顿距离) // 捕获循环变量(避免闭包陷阱)
int moveDistance = GameMap.CalculateManhattanDistance(startGrid, potentialMoveGrid); Grid currentMoveGrid = potentialMoveGrid;
double movePenalty = moveDistance * 0.5; // 每移动一步扣0.5分 Task task = Task.Run(async () =>
if (pNormalAttack > 0)
{ {
if (CanCharacterNormalAttack(character, dp)) await semaphore.WaitAsync();
try
{ {
// 计算普通攻击的可达格子 CalculateDecisionForGrid(
List<Grid> normalAttackReachableGrids = _map.GetGridsByRange(potentialMoveGrid, character.ATR, true); character, dp, startGrid, currentMoveGrid,
availableSkills, availableItems, allEnemysInGame, allTeammatesInGame,
List<Character> normalAttackReachableEnemys = [.. allEnemysInGame.Where(c => normalAttackReachableGrids.SelectMany(g => g.Characters).Contains(c) && !c.IsUnselectable && selectableEnemys.Contains(c)).Distinct()]; selectableEnemys, selectableTeammates,
List<Character> normalAttackReachableTeammates = [.. allTeammatesInGame.Where(c => normalAttackReachableGrids.SelectMany(g => g.Characters).Contains(c) && selectableTeammates.Contains(c)).Distinct()]; normalizedPUseItem, normalizedPCastSkill, normalizedPNormalAttack,
preferredAction, candidateDecisions
if (normalAttackReachableEnemys.Count > 0) );
{
// 将筛选后的目标列表传递给 SelectTargets
List<Character> 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
});
}
}
} }
} catch (Exception ex)
if (pCastSkill > 0)
{
foreach (Skill skill in availableSkills)
{ {
if (CanCharacterUseSkill(character, skill, dp) && _queue.CheckCanCast(character, skill, out double cost)) // 单个格子的计算异常不影响整体,记录日志
{ Console.WriteLine($"计算格子[{currentMoveGrid.X},{currentMoveGrid.Y}]决策失败:{ex.Message}");
// 计算当前技能的可达格子
List<Grid> 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<Character> skillReachableEnemys = [.. allEnemysInGame.Where(c => skillReachableGrids.SelectMany(g => g.Characters).Contains(c) && !c.IsUnselectable && selectableEnemys.Contains(c)).Distinct()];
List<Character> skillReachableTeammates = [.. allTeammatesInGame.Where(c => skillReachableGrids.SelectMany(g => g.Characters).Contains(c) && selectableTeammates.Contains(c)).Distinct()];
// 检查是否有可用的目标(敌人或队友,取决于技能类型)
if (skillReachableEnemys.Count > 0 || skillReachableTeammates.Count > 0)
{
// 将筛选后的目标列表传递给 SelectTargets
List<Character> 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
});
}
}
}
}
} }
} finally
if (pUseItem > 0)
{
foreach (Item item in availableItems)
{ {
if (item.Skills.Active != null && CanCharacterUseItem(character, item, dp) && _queue.CheckCanCast(character, item.Skills.Active, out double cost)) semaphore.Release();
{
Skill itemSkill = item.Skills.Active;
// 计算当前物品技能的可达格子
List<Grid> 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<Character> itemSkillReachableEnemys = [.. allEnemysInGame.Where(c => itemSkillReachableGrids.SelectMany(g => g.Characters).Contains(c) && !c.IsUnselectable && selectableEnemys.Contains(c)).Distinct()];
List<Character> itemSkillReachableTeammates = [.. allTeammatesInGame.Where(c => itemSkillReachableGrids.SelectMany(g => g.Characters).Contains(c) && selectableTeammates.Contains(c)).Distinct()];
// 检查是否有可用的目标
if (itemSkillReachableEnemys.Count > 0 || itemSkillReachableTeammates.Count > 0)
{
// 将筛选后的目标列表传递给 SelectTargets
List<Character> 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
});
}
}
}
}
} }
} });
decisionTasks.Add(task);
}
// 如果从该格子没有更好的行动,但移动本身有价值 // 等待所有异步任务完成
// 只有当当前最佳决策是“结束回合”或分数很低时,才考虑纯粹的移动 Task.WaitAll(decisionTasks);
if (potentialMoveGrid != startGrid && bestDecision.Score < 0) // 如果当前最佳决策是负分(即什么都不做)
{
double pureMoveScore = -movePenalty; // 移动本身有代价
// 为纯粹移动逻辑重新计算综合可达敌人列表 // 从所有候选决策中选出最高分的(保留原有评分权重逻辑)
List<Grid> tempAttackGridsForPureMove = _map.GetGridsByRange(potentialMoveGrid, character.ATR, true); if (!candidateDecisions.IsEmpty)
List<Grid> tempCastGridsForPureMove = []; {
foreach (Skill skill in availableSkills) bestDecision = candidateDecisions
{ .OrderByDescending(d => d.Score * d.ProbabilityWeight)
tempCastGridsForPureMove.AddRange(_map.GetGridsByRange(potentialMoveGrid, skill.CastRange, true)); .FirstOrDefault() ?? bestDecision;
}
foreach (Item item in availableItems)
{
if (item.Skills.Active != null)
{
tempCastGridsForPureMove.AddRange(_map.GetGridsByRange(potentialMoveGrid, item.Skills.Active.CastRange, true));
}
}
List<Grid> tempAllReachableGridsForPureMove = [.. tempAttackGridsForPureMove.Union(tempCastGridsForPureMove).Distinct()];
List<Character> 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;
} }
return bestDecision; return bestDecision;
} }
/// <summary>
/// 异步执行的核心:计算单个移动格子的所有可能决策,并添加到线程安全容器
/// </summary>
/// <param name="character"></param>
/// <param name="dp"></param>
/// <param name="startGrid"></param>
/// <param name="potentialMoveGrid"></param>
/// <param name="availableSkills"></param>
/// <param name="availableItems"></param>
/// <param name="allEnemysInGame"></param>
/// <param name="allTeammatesInGame"></param>
/// <param name="selectableEnemys"></param>
/// <param name="selectableTeammates"></param>
/// <param name="normalizedPUseItem"></param>
/// <param name="normalizedPCastSkill"></param>
/// <param name="normalizedPNormalAttack"></param>
/// <param name="preferredAction"></param>
/// <param name="candidateDecisions"></param>
private void CalculateDecisionForGrid(
Character character, DecisionPoints dp, Grid startGrid, Grid potentialMoveGrid,
List<Skill> availableSkills, List<Item> availableItems, List<Character> allEnemysInGame, List<Character> allTeammatesInGame,
List<Character> selectableEnemys, List<Character> selectableTeammates,
double normalizedPUseItem, double normalizedPCastSkill, double normalizedPNormalAttack,
CharacterActionType? preferredAction,
ConcurrentBag<AIDecision> candidateDecisions)
{
// 计算移动惩罚
int moveDistance = GameMap.CalculateManhattanDistance(startGrid, potentialMoveGrid);
double movePenalty = moveDistance * 0.5;
// 计算普通攻击决策
if (normalizedPNormalAttack > 0 && CanCharacterNormalAttack(character, dp))
{
List<Grid> normalAttackReachableGrids = _map.GetGridsByRange(potentialMoveGrid, character.ATR, true);
List<Character> normalAttackReachableEnemys = [.. allEnemysInGame.Where(c =>
normalAttackReachableGrids.SelectMany(g => g.Characters).Contains(c)
&& !c.IsUnselectable
&& selectableEnemys.Contains(c)).Distinct()];
if (normalAttackReachableEnemys.Count > 0)
{
List<Character> 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<Grid> 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<Character> skillReachableEnemys = [.. allEnemysInGame.Where(c =>
skillReachableGrids.SelectMany(g => g.Characters).Contains(c)
&& !c.IsUnselectable
&& selectableEnemys.Contains(c)).Distinct()];
List<Character> skillReachableTeammates = [.. allTeammatesInGame.Where(c =>
skillReachableGrids.SelectMany(g => g.Characters).Contains(c)
&& selectableTeammates.Contains(c)).Distinct()];
if (skillReachableEnemys.Count > 0 || skillReachableTeammates.Count > 0)
{
List<Character> 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<Grid> 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<Character> itemSkillReachableEnemys = [.. allEnemysInGame.Where(c =>
itemSkillReachableGrids.SelectMany(g => g.Characters).Contains(c)
&& !c.IsUnselectable
&& selectableEnemys.Contains(c)).Distinct()];
List<Character> itemSkillReachableTeammates = [.. allTeammatesInGame.Where(c =>
itemSkillReachableGrids.SelectMany(g => g.Characters).Contains(c)
&& selectableTeammates.Contains(c)).Distinct()];
if (itemSkillReachableEnemys.Count > 0 || itemSkillReachableTeammates.Count > 0)
{
List<Character> 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<Grid> tempAttackGridsForPureMove = _map.GetGridsByRange(potentialMoveGrid, character.ATR, true);
List<Grid> 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<Grid> tempAllReachableGridsForPureMove = [.. tempAttackGridsForPureMove.Union(tempCastGridsForPureMove).Distinct()];
List<Character> 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 决策辅助方法 --- // --- AI 决策辅助方法 ---
// 获取偏好行动类型 // 获取偏好行动类型
@ -343,42 +451,39 @@ namespace Milimoe.FunGame.Core.Controller
// 检查角色是否能进行普通攻击(基于状态) // 检查角色是否能进行普通攻击(基于状态)
private static bool CanCharacterNormalAttack(Character character, DecisionPoints dp) private static bool CanCharacterNormalAttack(Character character, DecisionPoints dp)
{ {
return dp.CheckActionTypeQuota(CharacterActionType.NormalAttack) && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostNormalAttack && return dp.CheckActionTypeQuota(CharacterActionType.NormalAttack)
character.CharacterState != CharacterState.NotActionable && && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostNormalAttack
character.CharacterState != CharacterState.ActionRestricted && && character.CharacterState != CharacterState.NotActionable
character.CharacterState != CharacterState.BattleRestricted && && character.CharacterState != CharacterState.ActionRestricted
character.CharacterState != CharacterState.AttackRestricted; && character.CharacterState != CharacterState.BattleRestricted
&& character.CharacterState != CharacterState.AttackRestricted;
} }
// 检查角色是否能使用某个技能(基于状态) // 检查角色是否能使用某个技能(基于状态)
private static bool CanCharacterUseSkill(Character character, Skill skill, DecisionPoints dp) private static bool CanCharacterUseSkill(Character character, Skill skill, DecisionPoints dp)
{ {
return ((skill.SkillType == SkillType.Magic && dp.CheckActionTypeQuota(CharacterActionType.PreCastSkill) && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostMagic) || return (
(skill.SkillType == SkillType.Skill && dp.CheckActionTypeQuota(CharacterActionType.CastSkill) && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostSkill) || (skill.SkillType == SkillType.Magic && dp.CheckActionTypeQuota(CharacterActionType.PreCastSkill) && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostMagic)
(skill.SkillType == SkillType.SuperSkill && dp.CheckActionTypeQuota(CharacterActionType.CastSuperSkill)) && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostSuperSkill) && || (skill.SkillType == SkillType.Skill && dp.CheckActionTypeQuota(CharacterActionType.CastSkill) && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostSkill)
character.CharacterState != CharacterState.NotActionable && || (skill.SkillType == SkillType.SuperSkill && dp.CheckActionTypeQuota(CharacterActionType.CastSuperSkill) && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostSuperSkill)
character.CharacterState != CharacterState.ActionRestricted && )
character.CharacterState != CharacterState.BattleRestricted && && character.CharacterState != CharacterState.NotActionable
character.CharacterState != CharacterState.SkillRestricted; && character.CharacterState != CharacterState.ActionRestricted
&& character.CharacterState != CharacterState.BattleRestricted
&& character.CharacterState != CharacterState.SkillRestricted;
} }
// 检查角色是否能使用某个物品(基于状态) // 检查角色是否能使用某个物品(基于状态)
private static bool CanCharacterUseItem(Character character, Item item, DecisionPoints dp) private static bool CanCharacterUseItem(Character character, Item item, DecisionPoints dp)
{ {
return dp.CheckActionTypeQuota(CharacterActionType.UseItem) && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostItem && return dp.CheckActionTypeQuota(CharacterActionType.UseItem)
character.CharacterState != CharacterState.NotActionable && && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostItem
(character.CharacterState != CharacterState.ActionRestricted || item.ItemType == ItemType.Consumable) && // 行动受限只能用消耗品 && character.CharacterState != CharacterState.NotActionable
character.CharacterState != CharacterState.BattleRestricted; && (character.CharacterState != CharacterState.ActionRestricted || item.ItemType == ItemType.Consumable)
&& character.CharacterState != CharacterState.BattleRestricted;
} }
/// <summary> // 选择技能的最佳目标
/// 选择技能的最佳目标
/// </summary>
/// <param name="character"></param>
/// <param name="skill"></param>
/// <param name="enemys"></param>
/// <param name="teammates"></param>
/// <returns></returns>
private static List<Character> SelectTargets(Character character, ISkill skill, List<Character> enemys, List<Character> teammates) private static List<Character> SelectTargets(Character character, ISkill skill, List<Character> enemys, List<Character> teammates)
{ {
List<Character> targets = skill.GetSelectableTargets(character, enemys, teammates); List<Character> targets = skill.GetSelectableTargets(character, enemys, teammates);
@ -386,12 +491,7 @@ namespace Milimoe.FunGame.Core.Controller
return [.. targets.OrderBy(o => Random.Shared.Next()).Take(count)]; return [.. targets.OrderBy(o => Random.Shared.Next()).Take(count)];
} }
/// <summary> // 评估普通攻击的价值
/// 评估普通攻击的价值
/// </summary>
/// <param name="character"></param>
/// <param name="targets"></param>
/// <returns></returns>
private static double EvaluateNormalAttack(Character character, List<Character> targets) private static double EvaluateNormalAttack(Character character, List<Character> targets)
{ {
double score = 0; double score = 0;
@ -401,24 +501,16 @@ namespace Milimoe.FunGame.Core.Controller
score += damage; score += damage;
if (target.HP <= damage) score += 100; if (target.HP <= damage) score += 100;
} }
score += EvaluateNormalAttackEvent?.Invoke(character, character.NormalAttack, targets) ?? 0;
return score; return score;
} }
/// <summary> // 评估技能的价值
/// 评估技能的价值
/// </summary>
/// <param name="character"></param>
/// <param name="skill"></param>
/// <param name="targets"></param>
/// <param name="cost"></param>
/// <returns></returns>
private static double EvaluateSkill(Character character, Skill skill, List<Character> targets, double cost) private static double EvaluateSkill(Character character, Skill skill, List<Character> targets, double cost)
{ {
double score = 0; double score = 0;
score += targets.Sum(t => CalculateTargetValue(t, skill)); score += targets.Sum(t => CalculateTargetValue(t, skill));
//score -= cost * 5; score += EvaluateSkillEvent?.Invoke(character, skill, targets, cost) ?? 0;
//score -= skill.RealCD * 2;
//score -= skill.HardnessTime * 2;
return score; return score;
} }
@ -455,6 +547,7 @@ namespace Milimoe.FunGame.Core.Controller
double movePenalty = GameMap.CalculateManhattanDistance(_map.GetCharacterCurrentGrid(character)!, moveGrid) * 0.5; double movePenalty = GameMap.CalculateManhattanDistance(_map.GetCharacterCurrentGrid(character)!, moveGrid) * 0.5;
double finalScore = bestSkillScore - movePenalty; double finalScore = bestSkillScore - movePenalty;
finalScore += EvaluateNonDirectionalSkillEvent?.Invoke(character, skill, moveGrid, castableGrids, allEnemys, allTeammates, cost) ?? 0;
return new AIDecision return new AIDecision
{ {
@ -463,33 +556,24 @@ namespace Milimoe.FunGame.Core.Controller
SkillToUse = skill, SkillToUse = skill,
Targets = [], Targets = [],
TargetGrids = bestTargetGrids, TargetGrids = bestTargetGrids,
Score = finalScore Score = finalScore,
ProbabilityWeight = 1.0 // 非指向性技能默认权重
}; };
} }
/// <summary> // 评估物品的价值
/// 评估物品的价值
/// </summary>
/// <param name="character"></param>
/// <param name="item"></param>
/// <param name="targets"></param>
/// <param name="cost"></param>
/// <returns></returns>
private static double EvaluateItem(Character character, Item item, List<Character> targets, double cost) private static double EvaluateItem(Character character, Item item, List<Character> targets, double cost)
{ {
double score = Random.Shared.Next(1000); double score = Random.Shared.Next(1000);
score += EvaluateItemEvent?.Invoke(character, item, targets, cost) ?? 0;
return score; return score;
} }
/// <summary> // 辅助函数:计算单个目标在某个技能下的价值
/// 辅助函数:计算单个目标在某个技能下的价值
/// </summary>
/// <param name="target"></param>
/// <param name="skill"></param>
/// <returns></returns>
private static double CalculateTargetValue(Character target, ISkill skill) private static double CalculateTargetValue(Character target, ISkill skill)
{ {
double value = Random.Shared.Next(1000); double value = Random.Shared.Next(1000);
value += CalculateTargetValueEvent?.Invoke(target, skill) ?? 0;
return value; return value;
} }
} }

View File

@ -306,13 +306,25 @@ namespace Milimoe.FunGame.Core.Entity
/// 在选取目标前向角色(玩家)发起询问 /// 在选取目标前向角色(玩家)发起询问
/// </summary> /// </summary>
/// <param name="character"></param> /// <param name="character"></param>
/// <param name="item"></param> /// <param name="dp"></param>
/// <returns></returns> /// <returns></returns>
public virtual InquiryOptions? InquiryBeforeTargetSelection(Character character, Item item) public virtual InquiryOptions? InquiryBeforeTargetSelection(Character character, DecisionPoints dp)
{ {
return null; return null;
} }
/// <summary>
/// 处理在选取目标前发起的询问的结果
/// </summary>
/// <param name="character"></param>
/// <param name="dp"></param>
/// <param name="options"></param>
/// <param name="response"></param>
public virtual void ResolveInquiryBeforeTargetSelection(Character character, DecisionPoints dp, InquiryOptions options, InquiryResponse response)
{
}
/// <summary> /// <summary>
/// 局内使用物品触发 /// 局内使用物品触发
/// </summary> /// </summary>

View File

@ -483,19 +483,43 @@ namespace Milimoe.FunGame.Core.Entity
/// </summary> /// </summary>
/// <param name="character"></param> /// <param name="character"></param>
/// <param name="normalAttack"></param> /// <param name="normalAttack"></param>
/// <param name="dp"></param>
/// <returns></returns> /// <returns></returns>
public delegate InquiryOptions? NormalAttackInquiryOptionsDelegate(Character character, NormalAttack normalAttack); public delegate InquiryOptions? NormalAttackInquiryOptionsDelegate(Character character, NormalAttack normalAttack, DecisionPoints dp);
public event NormalAttackInquiryOptionsDelegate? InquiryBeforeTargetSelectionEvent; public event NormalAttackInquiryOptionsDelegate? InquiryBeforeTargetSelectionEvent;
/// <summary> /// <summary>
/// 触发选择目标前的询问事件 /// 触发选择目标前的询问事件
/// </summary> /// </summary>
/// <param name="character"></param> /// <param name="character"></param>
/// <param name="normalAttack"></param> /// <param name="dp"></param>
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);
} }
/// <summary>
/// 处理在选取目标前发起询问的结果事件
/// </summary>
/// <param name="character"></param>
/// <param name="normalAttack"></param>
/// <param name="dp"></param>
/// <param name="options"></param>
/// <param name="response"></param>
public delegate void ResolveInquiryBeforeTargetSelection(Character character, NormalAttack normalAttack, DecisionPoints dp, InquiryOptions options, InquiryResponse response);
public event ResolveInquiryBeforeTargetSelection? ResolveInquiryBeforeTargetSelectionEvent;
/// <summary>
/// 触发处理选取目标前发起询问的结果事件
/// </summary>
/// <param name="character"></param>
/// <param name="dp"></param>
/// <param name="options"></param>
/// <param name="response"></param>
public void OnResolveInquiryBeforeTargetSelection(Character character, DecisionPoints dp, InquiryOptions options, InquiryResponse response)
{
ResolveInquiryBeforeTargetSelectionEvent?.Invoke(character, this, dp, options, response);
}
/// <summary> /// <summary>
/// 等级 /// 等级
/// </summary> /// </summary>

View File

@ -458,13 +458,25 @@ namespace Milimoe.FunGame.Core.Entity
/// 在选取目标前向角色(玩家)发起询问 /// 在选取目标前向角色(玩家)发起询问
/// </summary> /// </summary>
/// <param name="character"></param> /// <param name="character"></param>
/// <param name="skill"></param> /// <param name="dp"></param>
/// <returns></returns> /// <returns></returns>
public virtual InquiryOptions? InquiryBeforeTargetSelection(Character character, Skill skill) public virtual InquiryOptions? InquiryBeforeTargetSelection(Character character, DecisionPoints dp)
{ {
return null; return null;
} }
/// <summary>
/// 处理在选取目标前发起的询问的结果
/// </summary>
/// <param name="character"></param>
/// <param name="dp"></param>
/// <param name="options"></param>
/// <param name="response"></param>
public virtual void ResolveInquiryBeforeTargetSelection(Character character, DecisionPoints dp, InquiryOptions options, InquiryResponse response)
{
}
/// <summary> /// <summary>
/// 获取可选择的目标列表 /// 获取可选择的目标列表
/// </summary> /// </summary>

View File

@ -1310,8 +1310,6 @@ namespace Milimoe.FunGame.Core.Model
moved = CharacterMove(character, dp, aiDecision.TargetMoveGrid, startGrid); moved = CharacterMove(character, dp, aiDecision.TargetMoveGrid, startGrid);
} }
int costDP = dp.GetActionPointCost(type);
effects = [.. character.Effects.Where(e => e.IsInEffect).OrderByDescending(e => e.Priority)]; effects = [.. character.Effects.Where(e => e.IsInEffect).OrderByDescending(e => e.Priority)];
foreach (Effect effect in effects) foreach (Effect effect in effects)
{ {
@ -1346,6 +1344,16 @@ namespace Milimoe.FunGame.Core.Model
} }
else if (type == CharacterActionType.NormalAttack) 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 || if (!forceAction && (character.CharacterState == CharacterState.NotActionable ||
character.CharacterState == CharacterState.ActionRestricted || character.CharacterState == CharacterState.ActionRestricted ||
character.CharacterState == CharacterState.BattleRestricted || character.CharacterState == CharacterState.BattleRestricted ||
@ -1363,13 +1371,6 @@ namespace Milimoe.FunGame.Core.Model
} }
else else
{ {
// 使用普通攻击逻辑
// 如果有询问,先进行询问
character.NormalAttack.GamingQueue = this;
if (character.NormalAttack.OnInquiryBeforeTargetSelection(character, character.NormalAttack) is InquiryOptions inquiry)
{
Inquiry(character, inquiry);
}
// 选择目标 // 选择目标
List<Character> targets; List<Character> targets;
if (aiDecision != null) if (aiDecision != null)
@ -1446,21 +1447,22 @@ namespace Milimoe.FunGame.Core.Model
skill.GamingQueue = this; skill.GamingQueue = this;
List<Character> targets = []; List<Character> targets = [];
List<Grid> grids = []; List<Grid> grids = [];
costDP = dp.GetActionPointCost(type, skill); if (skill.SkillType == SkillType.Magic)
if (dp.CurrentDecisionPoints < costDP)
{ {
if (IsDebug) WriteLine($"[ {character} ] 想要释放 [ {skill.Name} ],但决策点不足,无法释放技能!"); // 如果有询问,先进行询问
} if (skill.InquiryBeforeTargetSelection(character, dp) is InquiryOptions inquiry)
else if (skill.SkillType == SkillType.Magic)
{
if (CheckCanCast(character, skill, out double cost))
{ {
// 如果有询问,先进行询问 InquiryResponse response = Inquiry(character, inquiry);
if (skill.InquiryBeforeTargetSelection(character, skill) is InquiryOptions inquiry) skill.ResolveInquiryBeforeTargetSelection(character, dp, inquiry, response);
{ }
Inquiry(character, inquiry);
}
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<Grid> castRange = []; List<Grid> castRange = [];
if (_map != null && realGrid != null) 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 else
{ {
// 只有魔法需要吟唱,战技和爆发技直接释放 // 如果有询问,先进行询问
if (CheckCanCast(character, skill, out double cost)) if (skill.InquiryBeforeTargetSelection(character, dp) is InquiryOptions inquiry)
{ {
// 如果有询问,先进行询问 InquiryResponse response = Inquiry(character, inquiry);
if (skill.InquiryBeforeTargetSelection(character, skill) is InquiryOptions inquiry) skill.ResolveInquiryBeforeTargetSelection(character, dp, inquiry, response);
{ }
Inquiry(character, inquiry);
}
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<Grid> castRange = []; List<Grid> castRange = [];
if (_map != null && realGrid != null) if (_map != null && realGrid != null)
{ {
@ -1686,7 +1690,6 @@ namespace Milimoe.FunGame.Core.Model
{ {
WriteLine($"[ {character} ] 想要释放 [ {skill.Name} ],但是没有目标!"); WriteLine($"[ {character} ] 想要释放 [ {skill.Name} ],但是没有目标!");
} }
_castingSkills.Remove(character);
WriteLine($"[ {character} ] 放弃释放技能!"); WriteLine($"[ {character} ] 放弃释放技能!");
character.CharacterState = CharacterState.Actionable; character.CharacterState = CharacterState.Actionable;
character.UpdateCharacterState(); character.UpdateCharacterState();
@ -1755,7 +1758,6 @@ namespace Milimoe.FunGame.Core.Model
} }
else else
{ {
_castingSuperSkills.Remove(character);
WriteLine($"[ {character} ] 因能量不足放弃释放爆发技!"); WriteLine($"[ {character} ] 因能量不足放弃释放爆发技!");
character.CharacterState = CharacterState.Actionable; character.CharacterState = CharacterState.Actionable;
character.UpdateCharacterState(); character.UpdateCharacterState();
@ -1787,14 +1789,17 @@ namespace Milimoe.FunGame.Core.Model
Skill skill = item.Skills.Active; 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<Grid> castRange = []; List<Grid> castRange = [];
@ -1804,6 +1809,8 @@ namespace Milimoe.FunGame.Core.Model
enemys = [.. enemys.Where(castRange.SelectMany(g => g.Characters).Contains)]; enemys = [.. enemys.Where(castRange.SelectMany(g => g.Characters).Contains)];
teammates = [.. teammates.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 (dp.CurrentDecisionPoints < costDP)
{ {
if (IsDebug) WriteLine($"[ {character} ] 想要使用物品 [ {item.Name} ],但决策点不足,无法使用物品!"); if (IsDebug) WriteLine($"[ {character} ] 想要使用物品 [ {item.Name} ],但决策点不足,无法使用物品!");