From 2c39f46dd936dd4f176d0d9c339412b3701f00d7 Mon Sep 17 00:00:00 2001 From: milimoe <110188673+milimoe@users.noreply.github.com> Date: Sat, 13 Sep 2025 15:10:11 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=88=98=E6=A3=8B=E5=9C=B0?= =?UTF-8?q?=E5=9B=BE=E7=8E=A9=E6=B3=95=20(#142)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Controller/AIController.cs | 322 +++++++++++++++++ Entity/Character/Character.cs | 104 +++++- Entity/Character/Unit.cs | 24 +- Entity/Item/Item.cs | 12 +- Entity/Skill/Effect.cs | 19 +- Entity/Skill/NormalAttack.cs | 6 + Entity/Skill/OpenSkill.cs | 46 +++ Entity/Skill/Skill.cs | 33 +- Entity/System/Team.cs | 14 +- Interface/Base/IGamingQueue.cs | 27 +- Interface/Entity/Typical/ISkill.cs | 6 + .../Common/Addon/Example/ExampleGameModule.cs | 2 +- Library/Common/Addon/GameMap.cs | 122 ++++++- .../JsonConverter/CharacterConverter.cs | 16 +- Library/Constant/TypeEnum.cs | 10 + Model/AIDecision.cs | 18 + Model/EquilibriumConstant.cs | 80 +++++ Model/GamingQueue.cs | 324 ++++++++++++++---- Model/TeamGamingQueue.cs | 6 +- 19 files changed, 1067 insertions(+), 124 deletions(-) create mode 100644 Controller/AIController.cs create mode 100644 Model/AIDecision.cs diff --git a/Controller/AIController.cs b/Controller/AIController.cs new file mode 100644 index 0000000..35b77a0 --- /dev/null +++ b/Controller/AIController.cs @@ -0,0 +1,322 @@ +using Milimoe.FunGame.Core.Entity; +using Milimoe.FunGame.Core.Interface.Entity; +using Milimoe.FunGame.Core.Library.Common.Addon; +using Milimoe.FunGame.Core.Library.Constant; +using Milimoe.FunGame.Core.Model; + +namespace Milimoe.FunGame.Core.Controller +{ + public class AIController(GamingQueue queue, GameMap map) + { + private readonly GamingQueue _queue = queue; + private readonly GameMap _map = map; + + /// + /// AI的核心决策方法,根据当前游戏状态为角色选择最佳行动。 + /// + /// 当前行动的AI角色。 + /// 角色的起始格子。 + /// 从起始格子可达的所有移动格子(包括起始格子本身)。 + /// 角色所有可用的技能(已过滤CD和EP/MP)。 + /// 角色所有可用的物品(已过滤CD和EP/MP)。 + /// 场上所有敌人。 + /// 场上所有队友。 + /// 包含最佳行动的AIDecision对象。 + public async Task DecideAIActionAsync(Character character, Grid startGrid, List allPossibleMoveGrids, + List availableSkills, List availableItems, List allEnemysInGame, List allTeammatesInGame) + { + // 初始化一个默认的“结束回合”决策作为基准 + AIDecision bestDecision = new() + { + ActionType = CharacterActionType.EndTurn, + TargetMoveGrid = startGrid, + Targets = [], + Score = -1000.0 + }; + + // 遍历所有可能的移动目标格子 (包括起始格子本身) + foreach (Grid potentialMoveGrid in allPossibleMoveGrids) + { + // 计算移动到这个格子的代价(曼哈顿距离) + int moveDistance = GameMap.CalculateManhattanDistance(startGrid, potentialMoveGrid); + double movePenalty = moveDistance * 0.5; // 每移动一步扣0.5分 + + if (CanCharacterNormalAttack(character)) + { + // 计算普通攻击的可达格子 + List normalAttackReachableGrids = _map.GetGridsByRange(potentialMoveGrid, character.ATR, true); + + List normalAttackReachableEnemys = [.. allEnemysInGame + .Where(c => normalAttackReachableGrids.SelectMany(g => g.Characters).Contains(c) && !c.IsUnselectable) + .Distinct()]; + List normalAttackReachableTeammates = [.. allTeammatesInGame + .Where(c => normalAttackReachableGrids.SelectMany(g => g.Characters).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; + if (currentScore > bestDecision.Score) + { + bestDecision = new AIDecision + { + ActionType = CharacterActionType.NormalAttack, + TargetMoveGrid = potentialMoveGrid, + SkillToUse = character.NormalAttack, + Targets = targets, + Score = currentScore + }; + } + } + } + } + + foreach (Skill skill in availableSkills) + { + if (CanCharacterUseSkill(character) && _queue.CheckCanCast(character, skill, out double cost)) + { + // 计算当前技能的可达格子 + List skillReachableGrids = _map.GetGridsByRange(potentialMoveGrid, skill.CastRange, true); + + List skillReachableEnemys = [.. allEnemysInGame + .Where(c => skillReachableGrids.SelectMany(g => g.Characters).Contains(c) && !c.IsUnselectable) + .Distinct()]; + List skillReachableTeammates = [.. allTeammatesInGame + .Where(c => skillReachableGrids.SelectMany(g => g.Characters).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; + if (currentScore > bestDecision.Score) + { + bestDecision = new AIDecision + { + ActionType = CharacterActionType.PreCastSkill, + TargetMoveGrid = potentialMoveGrid, + SkillToUse = skill, + Targets = targets, + Score = currentScore + }; + } + } + } + } + } + + foreach (Item item in availableItems) + { + if (item.Skills.Active != null && CanCharacterUseItem(character, item) && _queue.CheckCanCast(character, item.Skills.Active, out double cost)) + { + Skill itemSkill = item.Skills.Active; + + // 计算当前物品技能的可达格子 + List itemSkillReachableGrids = _map.GetGridsByRange(potentialMoveGrid, itemSkill.CastRange, true); + + List itemSkillReachableEnemys = [.. allEnemysInGame + .Where(c => itemSkillReachableGrids.SelectMany(g => g.Characters).Contains(c) && !c.IsUnselectable) + .Distinct()]; + List itemSkillReachableTeammates = [.. allTeammatesInGame + .Where(c => itemSkillReachableGrids.SelectMany(g => g.Characters).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; + if (currentScore > bestDecision.Score) + { + bestDecision = new AIDecision + { + ActionType = CharacterActionType.UseItem, + TargetMoveGrid = potentialMoveGrid, + ItemToUse = item, + SkillToUse = itemSkill, + Targets = targetsForItem, + Score = currentScore + }; + } + } + } + } + } + + // 如果从该格子没有更好的行动,但移动本身有价值 + // 只有当当前最佳决策是“结束回合”或分数很低时,才考虑纯粹的移动。 + if (potentialMoveGrid != startGrid && bestDecision.Score < 0) // 如果当前最佳决策是负分(即什么都不做) + { + 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) + .Distinct()]; + + // 如果当前位置无法攻击任何敌人,但地图上还有敌人,尝试向最近的敌人移动 + if (tempCurrentReachableEnemysForPureMove.Count == 0 && allEnemysInGame.Count > 0) // 使用新计算的列表 + { + Character? target = allEnemysInGame + .OrderBy(e => GameMap.CalculateManhattanDistance(potentialMoveGrid, _map.GetCharacterCurrentGrid(e)!)) + .FirstOrDefault(); + + if (target != null) + { + Grid? nearestEnemyGrid = _map.GetCharacterCurrentGrid(target); + if (nearestEnemyGrid != null) + { + // 奖励靠近敌人 + pureMoveScore += (10 - GameMap.CalculateManhattanDistance(potentialMoveGrid, nearestEnemyGrid)) * 0.1; + } + } + } + + // 如果纯粹移动比当前最佳(什么都不做)更好 + if (pureMoveScore > bestDecision.Score) + { + bestDecision = new AIDecision + { + ActionType = CharacterActionType.Move, + TargetMoveGrid = potentialMoveGrid, + Targets = [], + Score = pureMoveScore, + IsPureMove = true + }; + } + } + } + + return await Task.FromResult(bestDecision); + } + + // --- AI 决策辅助方法 --- + + // 检查角色是否能进行普通攻击(基于状态) + private static bool CanCharacterNormalAttack(Character character) + { + return character.CharacterState != CharacterState.NotActionable && + character.CharacterState != CharacterState.ActionRestricted && + character.CharacterState != CharacterState.BattleRestricted && + character.CharacterState != CharacterState.AttackRestricted; + } + + // 检查角色是否能使用某个技能(基于状态) + private static bool CanCharacterUseSkill(Character character) + { + return character.CharacterState != CharacterState.NotActionable && + character.CharacterState != CharacterState.ActionRestricted && + character.CharacterState != CharacterState.BattleRestricted && + character.CharacterState != CharacterState.SkillRestricted; + } + + // 检查角色是否能使用某个物品(基于状态) + private static bool CanCharacterUseItem(Character character, Item item) + { + return 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); + int count = skill.RealCanSelectTargetCount(enemys, teammates); + return [.. targets.OrderBy(o => Random.Shared.Next()).Take(count)]; + } + + /// + /// 评估普通攻击的价值 + /// + /// + /// + /// + private static double EvaluateNormalAttack(Character character, List targets) + { + double score = 0; + foreach (Character target in targets) + { + double damage = character.NormalAttack.Damage * (1 - target.PDR); + score += damage; + if (target.HP <= damage) score += 100; + } + 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; + return score; + } + + /// + /// 评估物品的价值 + /// + /// + /// + /// + /// + /// + private static double EvaluateItem(Character character, Item item, List targets, double cost) + { + double score = Random.Shared.Next(1000); + return score; + } + + /// + /// 辅助函数:计算单个目标在某个技能下的价值 + /// + /// + /// + /// + private static double CalculateTargetValue(Character target, ISkill skill) + { + double value = Random.Shared.Next(1000); + return value; + } + } +} diff --git a/Entity/Character/Character.cs b/Entity/Character/Character.cs index 139d405..64255c2 100644 --- a/Entity/Character/Character.cs +++ b/Entity/Character/Character.cs @@ -219,7 +219,7 @@ namespace Milimoe.FunGame.Core.Entity /// /// 最大生命值 = 基础生命值 + 额外生命值 + 额外生命值2 + 额外生命值3 /// - public double MaxHP => BaseHP + ExHP + ExHP2 + ExHP3; + public double MaxHP => Math.Max(1, BaseHP + ExHP + ExHP2 + ExHP3); /// /// 当前生命值 [ 战斗相关 ] @@ -238,6 +238,12 @@ namespace Milimoe.FunGame.Core.Entity } } + /// + /// 是否有魔法值 [ 初始设定 ] + /// + [InitRequired] + public bool HasMP { get; set; } = true; + /// /// 初始魔法值 [ 初始设定 ] /// @@ -272,7 +278,7 @@ namespace Milimoe.FunGame.Core.Entity /// /// 最大魔法值 = 基础魔法值 + 额外魔法值 + 额外魔法值2 + 额外魔法值3 /// - public double MaxMP => BaseMP + ExMP + ExMP2 + ExMP3; + public double MaxMP => Math.Max(1, BaseMP + ExMP + ExMP2 + ExMP3); /// /// 当前魔法值 [ 战斗相关 ] @@ -749,16 +755,68 @@ namespace Milimoe.FunGame.Core.Entity public double ExCDR { get; set; } = 0; /// - /// 攻击距离 [ 与技能和物品相关 ] [ 单位:格(半径) ] + /// 攻击距离 [ 与武器相关 ] [ 单位:格(半径) ] /// - [InitOptional] - public int ATR { get; set; } = 1; + public int ATR + { + get + { + int baseATR = 1; + if (EquipSlot.Weapon != null) + { + baseATR = EquipSlot.Weapon.WeaponType switch + { + WeaponType.OneHandedSword => GameplayEquilibriumConstant.OneHandedSwordAttackRange, + WeaponType.TwoHandedSword => GameplayEquilibriumConstant.TwoHandedSwordAttackRange, + WeaponType.Bow => GameplayEquilibriumConstant.BowAttackRange, + WeaponType.Pistol => GameplayEquilibriumConstant.PistolAttackRange, + WeaponType.Rifle => GameplayEquilibriumConstant.RifleAttackRange, + WeaponType.DualDaggers => GameplayEquilibriumConstant.DualDaggersAttackRange, + WeaponType.Talisman => GameplayEquilibriumConstant.TalismanAttackRange, + WeaponType.Staff => GameplayEquilibriumConstant.StaffAttackRange, + WeaponType.Polearm => GameplayEquilibriumConstant.PolearmAttackRange, + WeaponType.Gauntlet => GameplayEquilibriumConstant.GauntletAttackRange, + WeaponType.HiddenWeapon => GameplayEquilibriumConstant.HiddenWeaponAttackRange, + _ => baseATR + }; + } + return Math.Max(1, baseATR + ExATR); + } + } + + /// + /// 额外攻击距离 [ 与技能和物品相关 ] [ 单位:格(半径) ] + /// + public int ExATR { get; set; } = 0; + + /// + /// 行动力/可移动距离 [ 与第一定位相关 ] [ 单位:格(半径) ] + /// + public int MOV + { + get + { + int baseMOV = 3; + if (EquipSlot.Weapon != null) + { + baseMOV = FirstRoleType switch + { + RoleType.Core => GameplayEquilibriumConstant.RoleMOV_Core, + RoleType.Vanguard => GameplayEquilibriumConstant.RoleMOV_Vanguard, + RoleType.Guardian => GameplayEquilibriumConstant.RoleMOV_Guardian, + RoleType.Support => GameplayEquilibriumConstant.RoleMOV_Support, + RoleType.Medic => GameplayEquilibriumConstant.RoleMOV_Medic, + _ => baseMOV + }; + } + return Math.Max(1, baseMOV + ExMOV); + } + } /// /// 行动力/可移动距离 [ 与技能和物品相关 ] [ 单位:格(半径) ] /// - [InitOptional] - public int MOV { get; set; } = 5; + public int ExMOV { get; set; } = 0; /// /// 暴击率(%) = [ 与敏捷相关 ] + 额外暴击率(%) @@ -1388,7 +1446,7 @@ namespace Milimoe.FunGame.Core.Entity /// 获取角色的详细信息 /// /// - public string GetInfo(bool showUser = true, bool showGrowth = true, bool showEXP = false) + public string GetInfo(bool showUser = true, bool showGrowth = true, bool showEXP = false, bool showMapRelated = false) { StringBuilder builder = new(); @@ -1434,6 +1492,12 @@ namespace Milimoe.FunGame.Core.Entity builder.AppendLine($"魔法消耗减少:{INT * GameplayEquilibriumConstant.INTtoCastMPReduce * 100:0.##}%"); builder.AppendLine($"能量消耗减少:{INT * GameplayEquilibriumConstant.INTtoCastEPReduce * 100:0.##}%"); + if (showMapRelated) + { + builder.AppendLine($"移动距离:{MOV}"); + builder.AppendLine($"攻击距离:{ATR}"); + } + GetStatusInfo(builder); builder.AppendLine("== 普通攻击 =="); @@ -1475,7 +1539,7 @@ namespace Milimoe.FunGame.Core.Entity /// 获取角色的简略信息 /// /// - public string GetSimpleInfo(bool showUser = true, bool showGrowth = true, bool showEXP = false, bool showBasicOnly = false) + public string GetSimpleInfo(bool showUser = true, bool showGrowth = true, bool showEXP = false, bool showBasicOnly = false, bool showMapRelated = false) { StringBuilder builder = new(); @@ -1518,6 +1582,12 @@ namespace Milimoe.FunGame.Core.Entity builder.AppendLine($"生命回复:{HR:0.##}" + (ExHR != 0 ? $" [{InitialHR + STR * GameplayEquilibriumConstant.STRtoHRFactor:0.##} {(ExHR >= 0 ? "+" : "-")} {Math.Abs(ExHR):0.##}]" : "")); builder.AppendLine($"魔法回复:{MR:0.##}" + (ExMR != 0 ? $" [{InitialMR + INT * GameplayEquilibriumConstant.INTtoMRFactor:0.##} {(ExMR >= 0 ? "+" : "-")} {Math.Abs(ExMR):0.##}]" : "")); + if (showMapRelated) + { + builder.AppendLine($"移动距离:{MOV}"); + builder.AppendLine($"攻击距离:{ATR}"); + } + if (!showBasicOnly) { GetStatusInfo(builder); @@ -1685,7 +1755,7 @@ namespace Milimoe.FunGame.Core.Entity /// 获取角色的物品信息 /// /// - public string GetItemInfo(bool showUser = true, bool showGrowth = true, bool showEXP = false) + public string GetItemInfo(bool showUser = true, bool showGrowth = true, bool showEXP = false, bool showMapRelated = false) { StringBuilder builder = new(); @@ -1731,6 +1801,12 @@ namespace Milimoe.FunGame.Core.Entity builder.AppendLine($"魔法消耗减少:{INT * GameplayEquilibriumConstant.INTtoCastMPReduce * 100:0.##}%"); builder.AppendLine($"能量消耗减少:{INT * GameplayEquilibriumConstant.INTtoCastEPReduce * 100:0.##}%"); + if (showMapRelated) + { + builder.AppendLine($"移动距离:{MOV}"); + builder.AppendLine($"攻击距离:{ATR}"); + } + if (EquipSlot.Any()) { builder.AppendLine(GetEquipSlotInfo().Trim()); @@ -2022,8 +2098,8 @@ namespace Milimoe.FunGame.Core.Entity c.MDF = MDF.Copy(); c.Lifesteal = Lifesteal; c.Shield = Shield.Copy(); - c.ATR = ATR; - c.MOV = MOV; + c.ExATR = ExATR; + c.ExMOV = ExMOV; c.MagicType = MagicType; c.ImmuneType = ImmuneType; } @@ -2132,8 +2208,8 @@ namespace Milimoe.FunGame.Core.Entity ExActionCoefficient = c.ExActionCoefficient; ExAccelerationCoefficient = c.ExAccelerationCoefficient; ExCDR = c.ExCDR; - ATR = c.ATR; - MOV = c.MOV; + ExATR = c.ExATR; + ExMOV = c.ExMOV; ExCritRate = c.ExCritRate; ExCritDMG = c.ExCritDMG; ExEvadeRate = c.ExEvadeRate; diff --git a/Entity/Character/Unit.cs b/Entity/Character/Unit.cs index bd0cb18..08e75fa 100644 --- a/Entity/Character/Unit.cs +++ b/Entity/Character/Unit.cs @@ -47,7 +47,7 @@ namespace Milimoe.FunGame.Core.Entity /// 获取单位的详细信息 /// /// - public new string GetInfo(bool showUser = true, bool showGrowth = true, bool showEXP = false) + public new string GetInfo(bool showUser = true, bool showGrowth = true, bool showEXP = false, bool showMapRelated = false) { StringBuilder builder = new(); @@ -78,6 +78,12 @@ namespace Milimoe.FunGame.Core.Entity builder.AppendLine($"物理穿透:{PhysicalPenetration * 100:0.##}%"); builder.AppendLine($"魔法穿透:{MagicalPenetration * 100:0.##}%"); + if (showMapRelated) + { + builder.AppendLine($"移动距离:{MOV}"); + builder.AppendLine($"攻击距离:{ATR}"); + } + if (CharacterState != CharacterState.Actionable) { builder.AppendLine(CharacterSet.GetCharacterState(CharacterState)); @@ -174,7 +180,7 @@ namespace Milimoe.FunGame.Core.Entity /// 获取单位的简略信息 /// /// - public new string GetSimpleInfo(bool showUser = true, bool showGrowth = true, bool showEXP = false, bool showBasicOnly = false) + public new string GetSimpleInfo(bool showUser = true, bool showGrowth = true, bool showEXP = false, bool showBasicOnly = false, bool showMapRelated = false) { StringBuilder builder = new(); @@ -198,6 +204,12 @@ namespace Milimoe.FunGame.Core.Entity builder.AppendLine($"生命回复:{HR:0.##}" + (ExHR != 0 ? $" [{InitialHR + STR * GameplayEquilibriumConstant.STRtoHRFactor:0.##} {(ExHR >= 0 ? "+" : "-")} {Math.Abs(ExHR):0.##}]" : "")); builder.AppendLine($"魔法回复:{MR:0.##}" + (ExMR != 0 ? $" [{InitialMR + INT * GameplayEquilibriumConstant.INTtoMRFactor:0.##} {(ExMR >= 0 ? "+" : "-")} {Math.Abs(ExMR):0.##}]" : "")); + if (showMapRelated) + { + builder.AppendLine($"移动距离:{MOV}"); + builder.AppendLine($"攻击距离:{ATR}"); + } + if (!showBasicOnly) { if (CharacterState != CharacterState.Actionable) @@ -408,7 +420,7 @@ namespace Milimoe.FunGame.Core.Entity /// 获取单位的物品信息 /// /// - public new string GetItemInfo(bool showUser = true, bool showGrowth = true, bool showEXP = false) + public new string GetItemInfo(bool showUser = true, bool showGrowth = true, bool showEXP = false, bool showMapRelated = false) { StringBuilder builder = new(); @@ -439,6 +451,12 @@ namespace Milimoe.FunGame.Core.Entity builder.AppendLine($"物理穿透:{PhysicalPenetration * 100:0.##}%"); builder.AppendLine($"魔法穿透:{MagicalPenetration * 100:0.##}%"); + if (showMapRelated) + { + builder.AppendLine($"移动距离:{MOV}"); + builder.AppendLine($"攻击距离:{ATR}"); + } + if (EquipSlot.Any()) { builder.AppendLine("== 装备栏 =="); diff --git a/Entity/Item/Item.cs b/Entity/Item/Item.cs index 7d0ca7b..5d3cfbb 100644 --- a/Entity/Item/Item.cs +++ b/Entity/Item/Item.cs @@ -1,7 +1,9 @@ -using System.Text; +using System.Collections.Generic; +using System.Text; using Milimoe.FunGame.Core.Api.Utility; using Milimoe.FunGame.Core.Interface.Base; using Milimoe.FunGame.Core.Interface.Entity; +using Milimoe.FunGame.Core.Library.Common.Addon; using Milimoe.FunGame.Core.Library.Constant; namespace Milimoe.FunGame.Core.Entity @@ -310,7 +312,13 @@ namespace Milimoe.FunGame.Core.Entity } if (result && Skills.Active != null) { - used = await queue.UseItemAsync(this, character, enemys, teammates); + List castRange = []; + if (Skills.Active.GamingQueue != null && Skills.Active.GamingQueue.Map != null) + { + Grid? grid = Skills.Active.GamingQueue.Map.GetCharacterCurrentGrid(character); + castRange = grid is null ? [] : Skills.Active.GamingQueue.Map.GetGridsByRange(grid, Skills.Active.CastRange, true); + } + used = await queue.UseItemAsync(this, character, enemys, teammates, castRange); } if (used) { diff --git a/Entity/Skill/Effect.cs b/Entity/Skill/Effect.cs index 3946ffc..4528c10 100644 --- a/Entity/Skill/Effect.cs +++ b/Entity/Skill/Effect.cs @@ -1,4 +1,5 @@ -using System.Text; +using System.Collections.Generic; +using System.Text; using Milimoe.FunGame.Core.Api.Utility; using Milimoe.FunGame.Core.Interface.Base; using Milimoe.FunGame.Core.Interface.Entity; @@ -560,7 +561,8 @@ namespace Milimoe.FunGame.Core.Entity /// /// /// - public virtual void BeforeSelectTargetGrid(Character character, List enemys, List teammates, GameMap map) + /// + public virtual void BeforeSelectTargetGrid(Character character, List enemys, List teammates, GameMap map, List moveRange) { } @@ -1067,6 +1069,19 @@ namespace Milimoe.FunGame.Core.Entity return GamingQueue?.IsCharacterInAIControlling(character) ?? false; } + /// + /// 添加角色应用的特效类型到回合记录中 + /// + /// + /// + public void RecordCharacterApplyEffects(Character character, params List types) + { + if (GamingQueue?.LastRound.ApplyEffects.TryAdd(character, types) ?? false) + { + GamingQueue?.LastRound.ApplyEffects[character].AddRange(types); + } + } + /// /// 返回特效详情 /// diff --git a/Entity/Skill/NormalAttack.cs b/Entity/Skill/NormalAttack.cs index 25f3af3..6623f85 100644 --- a/Entity/Skill/NormalAttack.cs +++ b/Entity/Skill/NormalAttack.cs @@ -233,6 +233,11 @@ namespace Milimoe.FunGame.Core.Entity /// public double CurrentCD => 0; + /// + /// 游戏中的行动顺序表实例,使用时需要判断其是否存在 + /// + public IGamingQueue? GamingQueue { get; set; } = null; + /// /// 绑定到特效的普通攻击扩展。键为特效,值为对应的普攻扩展对象。 /// @@ -450,6 +455,7 @@ namespace Milimoe.FunGame.Core.Entity builder.AppendLine($"{Name} - 等级 {Level}"); builder.AppendLine($"描述:{Description}"); + if (GamingQueue?.Map != null) builder.AppendLine($"攻击距离:{Character.ATR}"); builder.AppendLine($"硬直时间:{RealHardnessTime:0.##}{(showOriginal && RealHardnessTime != HardnessTime ? $"(原始值:{HardnessTime})" : "")}"); return builder.ToString(); diff --git a/Entity/Skill/OpenSkill.cs b/Entity/Skill/OpenSkill.cs index 1273413..e6f39f3 100644 --- a/Entity/Skill/OpenSkill.cs +++ b/Entity/Skill/OpenSkill.cs @@ -44,6 +44,21 @@ namespace Milimoe.FunGame.Core.Entity CanSelectTeammate = teammate; } break; + case "allenemy": + case "allenemys": + case "allenemies": + if (bool.TryParse(args[str].ToString(), out bool allenemy)) + { + SelectAllEnemies = allenemy; + } + break; + case "allteammate": + case "allteammates": + if (bool.TryParse(args[str].ToString(), out bool allteammate)) + { + SelectAllTeammates = allteammate; + } + break; case "count": if (int.TryParse(args[str].ToString(), out int count) && count > 0) { @@ -56,6 +71,19 @@ namespace Milimoe.FunGame.Core.Entity CanSelectTargetRange = range; } break; + case "nd": + case "nondirectional": + if (bool.TryParse(args[str].ToString(), out bool nondirectional)) + { + IsNonDirectional = nondirectional; + } + break; + case "rangetype": + if (int.TryParse(args[str].ToString(), out int rangetype) && rangetype > 0) + { + SkillRangeType = (SkillRangeType)rangetype; + } + break; case "mpcost": if (double.TryParse(args[str].ToString(), out double mpcost) && mpcost > 0) { @@ -69,12 +97,14 @@ namespace Milimoe.FunGame.Core.Entity } break; case "costall": + case "costallep": if (bool.TryParse(args[str].ToString(), out bool costall) && costall) { CostAllEP = costall; } break; case "mincost": + case "mincostep": if (double.TryParse(args[str].ToString(), out double mincost) && mincost > 0) { MinCostEP = mincost; @@ -87,12 +117,28 @@ namespace Milimoe.FunGame.Core.Entity } break; case "cast": + case "casttime": if (double.TryParse(args[str].ToString(), out double cast) && cast > 0) { CastTime = cast; } break; + case "cr": + case "castrange": + if (int.TryParse(args[str].ToString(), out int castrange) && castrange > 0) + { + CastRange = castrange; + } + break; + case "caw": + case "castanywhere": + if (bool.TryParse(args[str].ToString(), out bool castanywhere)) + { + CastAnywhere = castanywhere; + } + break; case "ht": + case "hardnesstime": if (double.TryParse(args[str].ToString(), out double ht) && ht > 0) { HardnessTime = ht; diff --git a/Entity/Skill/Skill.cs b/Entity/Skill/Skill.cs index 65c315c..808bb39 100644 --- a/Entity/Skill/Skill.cs +++ b/Entity/Skill/Skill.cs @@ -99,7 +99,11 @@ namespace Milimoe.FunGame.Core.Entity /// 施法距离 [ 单位:格 ] /// [InitOptional] - public int CastRange { get; set; } = 5; + public int CastRange + { + get => Math.Max(1, CastAnywhere ? (GamingQueue?.Map != null ? GamingQueue.Map.Grids.Count : 999) : _CastRange); + set => _CastRange = Math.Max(1, value); + } /// /// 可选取自身 @@ -136,6 +140,24 @@ namespace Milimoe.FunGame.Core.Entity /// public virtual int CanSelectTargetRange { get; set; } = 0; + /// + /// 如果为 true,表示非指向性技能,可以任意选取一个范围( = 0 时为单个格子)。 + /// 如果为 false,表示必须选取一个角色作为目标,当 > 0 时,技能作用范围将根据目标位置覆盖 形状的区域;= 0 时正常选取目标。 + /// + public virtual bool IsNonDirectional { get; set; } = false; + + /// + /// 作用范围形状 + /// - 菱形。默认的曼哈顿距离正方形 + /// - 圆形。基于欧几里得距离的圆形 + /// - 正方形 + /// - 施法者与目标之前的直线 + /// - 施法者与目标所在的直线,贯穿至地图边缘 + /// - 扇形 + /// 注意,该属性不影响选取目标的范围。选取目标的范围由 决定。 + /// + public virtual SkillRangeType SkillRangeType { get; set; } = SkillRangeType.Diamond; + /// /// 选取角色的条件 /// @@ -534,6 +556,10 @@ namespace Milimoe.FunGame.Core.Entity { builder.AppendLine($"{DispelDescription}"); } + if (GamingQueue?.Map != null && SkillType != SkillType.Passive) + { + builder.AppendLine($"施法距离:{(CastAnywhere ? "全图" : CastRange)}"); + } if (IsActive && (Item?.IsInGameItem ?? true)) { if (SkillType == SkillType.Item) @@ -661,5 +687,10 @@ namespace Milimoe.FunGame.Core.Entity /// 等级 /// private int _Level = 0; + + /// + /// 施法距离 + /// + private int _CastRange = 3; } } diff --git a/Entity/System/Team.cs b/Entity/System/Team.cs index 0fb82a0..af41865 100644 --- a/Entity/System/Team.cs +++ b/Entity/System/Team.cs @@ -1,19 +1,17 @@ -using Milimoe.FunGame.Core.Interface.Base; - -namespace Milimoe.FunGame.Core.Entity +namespace Milimoe.FunGame.Core.Entity { public class Team(string name, IEnumerable charaters) { public Guid Id { get; set; } = Guid.Empty; public string Name { get; set; } = name; - public List Members { get; } = new(charaters); + public List Members { get; } = [.. charaters]; public int Score { get; set; } = 0; public bool IsWinner { get; set; } = false; public int Count => Members.Count; - public List GetActiveCharacters(IGamingQueue queue) + public List GetActiveCharacters() { - return [.. Members.Where(queue.Queue.Contains)]; + return [.. Members.Where(c => c.HP > 0)]; } public List GetTeammates(Character character) @@ -21,9 +19,9 @@ namespace Milimoe.FunGame.Core.Entity return [.. Members.Where(c => c != character)]; } - public List GetActiveTeammates(IGamingQueue queue, Character character) + public List GetActiveTeammates(Character character) { - return [.. Members.Where(c => queue.Queue.Contains(c) && c != character)]; + return [.. Members.Where(c => c.HP > 0 && c != character)]; } public bool IsOnThisTeam(Character character) diff --git a/Interface/Base/IGamingQueue.cs b/Interface/Base/IGamingQueue.cs index 695bf54..515f08c 100644 --- a/Interface/Base/IGamingQueue.cs +++ b/Interface/Base/IGamingQueue.cs @@ -25,6 +25,11 @@ namespace Milimoe.FunGame.Core.Interface.Base /// public Dictionary Original { get; } + /// + /// 参与本次游戏的所有角色列表 + /// + public List AllCharacters { get; } + /// /// 当前的行动顺序 /// @@ -151,8 +156,19 @@ namespace Milimoe.FunGame.Core.Interface.Base /// /// /// + /// + /// /// - public Task UseItemAsync(Item item, Character caster, List enemys, List teammates); + public Task UseItemAsync(Item item, Character caster, List enemys, List teammates, List castRange, List? desiredTargets = null); + + /// + /// 角色移动 + /// + /// + /// + /// + /// + public Task CharacterMoveAsync(Character character, Grid target, Grid? startGrid); /// /// 选取移动目标 @@ -161,8 +177,9 @@ namespace Milimoe.FunGame.Core.Interface.Base /// /// /// + /// /// - public Task SelectTargetGridAsync(Character character, List enemys, List teammates, GameMap map); + public Task SelectTargetGridAsync(Character character, List enemys, List teammates, GameMap map, List moveRange); /// /// 选取技能目标 @@ -171,8 +188,9 @@ namespace Milimoe.FunGame.Core.Interface.Base /// /// /// + /// /// - public Task> SelectTargetsAsync(Character caster, Skill skill, List enemys, List teammates); + public Task> SelectTargetsAsync(Character caster, Skill skill, List enemys, List teammates, List castRange); /// /// 选取普通攻击目标 @@ -181,8 +199,9 @@ namespace Milimoe.FunGame.Core.Interface.Base /// /// /// + /// /// - public Task> SelectTargetsAsync(Character character, NormalAttack attack, List enemys, List teammates); + public Task> SelectTargetsAsync(Character character, NormalAttack attack, List enemys, List teammates, List attackRange); /// /// 判断目标对于某个角色是否是队友 diff --git a/Interface/Entity/Typical/ISkill.cs b/Interface/Entity/Typical/ISkill.cs index 8d1ce21..cfea5ff 100644 --- a/Interface/Entity/Typical/ISkill.cs +++ b/Interface/Entity/Typical/ISkill.cs @@ -1,4 +1,5 @@ using Milimoe.FunGame.Core.Entity; +using Milimoe.FunGame.Core.Interface.Base; namespace Milimoe.FunGame.Core.Interface.Entity { @@ -7,6 +8,11 @@ namespace Milimoe.FunGame.Core.Interface.Entity /// public interface ISkill : IBaseEntity { + /// + /// 所属的行动顺序表实例 + /// + public IGamingQueue? GamingQueue { get; } + /// /// 此技能所属的角色 /// diff --git a/Library/Common/Addon/Example/ExampleGameModule.cs b/Library/Common/Addon/Example/ExampleGameModule.cs index 2ad54bd..5c959c4 100644 --- a/Library/Common/Addon/Example/ExampleGameModule.cs +++ b/Library/Common/Addon/Example/ExampleGameModule.cs @@ -344,7 +344,7 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example return map; } - private async Task Gq_SelectTargetGrid(GamingQueue queue, Character character, List enemys, List teammates, GameMap map) + private async Task Gq_SelectTargetGrid(GamingQueue queue, Character character, List enemys, List teammates, GameMap map, List canMoveGrids) { // 介入选择,假设这里更新界面,让玩家选择目的地 await Task.CompletedTask; diff --git a/Library/Common/Addon/GameMap.cs b/Library/Common/Addon/GameMap.cs index e1150e1..b054f40 100644 --- a/Library/Common/Addon/GameMap.cs +++ b/Library/Common/Addon/GameMap.cs @@ -240,6 +240,50 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon return grids; } + /// + /// 获取以某个格子为中心,最远距离的格子(曼哈顿距离),只考虑同一平面的格子。 + /// + /// + /// + /// + /// + public virtual List GetOuterGridsByRange(Grid grid, int range, bool includeCharacter = false) + { + List grids = []; + + if (range < 0) + { + return grids; + } + + // 遍历以中心格子为中心的方形区域 + // dx和dy的范围从 -range 到 +range + for (int dx = -range; dx <= range; ++dx) + { + for (int dy = -range; dy <= range; ++dy) + { + // 只有当曼哈顿距离恰好等于 range 时,才认为是最远距离的格子 + if (Math.Abs(dx) + Math.Abs(dy) == range) + { + int x = grid.X + dx; + int y = grid.Y + dy; + int z = grid.Z; // 只考虑同一平面 + + // 检查格子是否存在于地图中 + if (GridsByCoordinate.TryGetValue((x, y, z), out Grid? select) && select != null) + { + if (includeCharacter || select.Characters.Count == 0) + { + grids.Add(select); + } + } + } + } + } + + return grids; + } + /// /// 获取以某个格子为中心,一定半径内的格子(圆形范围,欧几里得距离),只考虑同一平面的格子。 /// @@ -254,7 +298,7 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon // 预计算半径的平方 int rangeSquared = range * range; - // 遍历以中心格子为中心的方形区域 + // 遍历以中心格子为中心的区域 // 范围从 -range 到 +range,覆盖所有可能的圆形区域内的格子 for (int dx = -range; dx <= range; ++dx) { @@ -281,6 +325,47 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon return grids; } + /// + /// 获取以某个格子为中心,最远距离的格子(圆形范围,欧几里得距离),只考虑同一平面的格子。 + /// + /// + /// + /// + /// + public virtual List GetOuterGridsByCircleRange(Grid grid, int range, bool includeCharacter = false) + { + List grids = []; + + // 预计算半径的平方 + int rangeSquared = range * range; + + // 遍历以中心格子为中心的区域 + // 范围从 -range 到 +range,覆盖所有可能的圆形区域内的格子 + for (int dx = -range; dx <= range; ++dx) + { + for (int dy = -range; dy <= range; ++dy) + { + // 计算当前格子与中心格子的欧几里得距离的平方 + if ((dx * dx) + (dy * dy) == rangeSquared) + { + int x = grid.X + dx; + int y = grid.Y + dy; + int z = grid.Z; + + if (GridsByCoordinate.TryGetValue((x, y, z), out Grid? select) && select != null) + { + if (includeCharacter || select.Characters.Count == 0) + { + grids.Add(select); + } + } + } + } + } + + return grids; + } + /// /// 设置角色移动 /// @@ -296,9 +381,15 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon } Grid? realGrid = GetCharacterCurrentGrid(character); - - if (current.Id == target.Id) + Grid startGrid = current; + if (realGrid != null && current.Id != realGrid.Id) { + startGrid = realGrid; + } + + if (startGrid.Id == target.Id) + { + SetCharacterCurrentGrid(character, startGrid); return 0; } @@ -308,8 +399,8 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon HashSet visited = []; // 将起始格子加入队列,步数为0,并标记为已访问 - queue.Enqueue((current, 0)); - visited.Add(current.Id); + queue.Enqueue((startGrid, 0)); + visited.Add(startGrid.Id); while (queue.Count > 0) { @@ -319,9 +410,7 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon if (currentGrid.Id == target.Id) { realGrid?.Characters.Remove(character); - current.Characters.Remove(character); - target.Characters.Add(character); - Characters[character] = target; + SetCharacterCurrentGrid(character, target); return currentSteps; } @@ -369,10 +458,15 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon } Grid? realGrid = GetCharacterCurrentGrid(character); - - if (current.Id == target.Id) + Grid startGrid = current; + if (realGrid != null && current.Id != realGrid.Id) { - SetCharacterCurrentGrid(character, current); + startGrid = realGrid; + } + + if (startGrid.Id == target.Id) + { + SetCharacterCurrentGrid(character, startGrid); return 0; } @@ -383,11 +477,11 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon HashSet visited = []; // 初始化 BFS 队列,将起始格子加入,步数为0 - queue.Enqueue((current, 0)); - visited.Add(current.Id); + queue.Enqueue((startGrid, 0)); + visited.Add(startGrid.Id); Grid? bestReachableGrid = current; - int minDistanceToTarget = CalculateManhattanDistance(current, target); + int minDistanceToTarget = CalculateManhattanDistance(startGrid, target); int stepsToBestReachable = 0; // 定义平面移动的四个方向 diff --git a/Library/Common/JsonConverter/CharacterConverter.cs b/Library/Common/JsonConverter/CharacterConverter.cs index abca358..ff6beff 100644 --- a/Library/Common/JsonConverter/CharacterConverter.cs +++ b/Library/Common/JsonConverter/CharacterConverter.cs @@ -83,6 +83,9 @@ namespace Milimoe.FunGame.Core.Library.Common.JsonConverter case nameof(Character.ExHPPercentage): result.ExHPPercentage = reader.GetDouble(); break; + case nameof(Character.HasMP): + result.HasMP = reader.GetBoolean(); + break; case nameof(Character.InitialMP): result.InitialMP = reader.GetDouble(); break; @@ -197,11 +200,11 @@ namespace Milimoe.FunGame.Core.Library.Common.JsonConverter case nameof(Character.ExCDR): result.ExCDR = reader.GetDouble(); break; - case nameof(Character.ATR): - result.ATR = reader.GetInt32(); + case nameof(Character.ExATR): + result.ExATR = reader.GetInt32(); break; - case nameof(Character.MOV): - result.MOV = reader.GetInt32(); + case nameof(Character.ExMOV): + result.ExMOV = reader.GetInt32(); break; case nameof(Character.ExCritRate): result.ExCritRate = reader.GetDouble(); @@ -273,6 +276,7 @@ namespace Milimoe.FunGame.Core.Library.Common.JsonConverter writer.WriteNumber(nameof(Character.InitialHP), value.InitialHP); writer.WriteNumber(nameof(Character.ExHP2), value.ExHP2); writer.WriteNumber(nameof(Character.ExHPPercentage), value.ExHPPercentage); + writer.WriteBoolean(nameof(Character.HasMP), value.HasMP); writer.WriteNumber(nameof(Character.InitialMP), value.InitialMP); writer.WriteNumber(nameof(Character.ExMP2), value.ExMP2); writer.WriteNumber(nameof(Character.ExMPPercentage), value.ExMPPercentage); @@ -311,8 +315,8 @@ namespace Milimoe.FunGame.Core.Library.Common.JsonConverter writer.WriteNumber(nameof(Character.ExActionCoefficient), value.ExActionCoefficient); writer.WriteNumber(nameof(Character.ExAccelerationCoefficient), value.ExAccelerationCoefficient); writer.WriteNumber(nameof(Character.ExCDR), value.ExCDR); - writer.WriteNumber(nameof(Character.ATR), value.ATR); - writer.WriteNumber(nameof(Character.MOV), value.MOV); + writer.WriteNumber(nameof(Character.ExATR), value.ExATR); + writer.WriteNumber(nameof(Character.ExMOV), value.ExMOV); writer.WriteNumber(nameof(Character.ExCritRate), value.ExCritRate); writer.WriteNumber(nameof(Character.ExCritDMG), value.ExCritDMG); writer.WriteNumber(nameof(Character.ExEvadeRate), value.ExEvadeRate); diff --git a/Library/Constant/TypeEnum.cs b/Library/Constant/TypeEnum.cs index 8db44a1..b1fb6f3 100644 --- a/Library/Constant/TypeEnum.cs +++ b/Library/Constant/TypeEnum.cs @@ -1063,4 +1063,14 @@ namespace Milimoe.FunGame.Core.Library.Constant Magical, True } + + public enum SkillRangeType + { + Diamond, + Circle, + Square, + Line, + LinePass, + Sector + } } diff --git a/Model/AIDecision.cs b/Model/AIDecision.cs new file mode 100644 index 0000000..1a188b2 --- /dev/null +++ b/Model/AIDecision.cs @@ -0,0 +1,18 @@ +using Milimoe.FunGame.Core.Entity; +using Milimoe.FunGame.Core.Interface.Entity; +using Milimoe.FunGame.Core.Library.Common.Addon; +using Milimoe.FunGame.Core.Library.Constant; + +namespace Milimoe.FunGame.Core.Model +{ + public class AIDecision + { + public CharacterActionType ActionType { get; set; } = CharacterActionType.EndTurn; + public Grid? TargetMoveGrid { get; set; } = null; + public ISkill? SkillToUse { get; set; } = null; + public Item? ItemToUse { get; set; } = null; + public List Targets { get; set; } = []; + public double Score { get; set; } = 0; + public bool IsPureMove { get; set; } = false; + } +} diff --git a/Model/EquilibriumConstant.cs b/Model/EquilibriumConstant.cs index 04c550e..e58db8b 100644 --- a/Model/EquilibriumConstant.cs +++ b/Model/EquilibriumConstant.cs @@ -460,6 +460,86 @@ namespace Milimoe.FunGame.Core.Model /// public double HiddenWeaponHardness { get; set; } = 7; + /// + /// 单手剑的攻击距离 + /// + public int OneHandedSwordAttackRange { get; set; } = 1; + + /// + /// 双手剑的攻击距离 + /// + public int TwoHandedSwordAttackRange { get; set; } = 2; + + /// + /// 弓的攻击距离 + /// + public int BowAttackRange { get; set; } = 4; + + /// + /// 手枪的攻击距离 + /// + public int PistolAttackRange { get; set; } = 3; + + /// + /// 步枪的攻击距离 + /// + public int RifleAttackRange { get; set; } = 5; + + /// + /// 双持短刀的攻击距离 + /// + public int DualDaggersAttackRange { get; set; } = 1; + + /// + /// 法器的攻击距离 + /// + public int TalismanAttackRange { get; set; } = 5; + + /// + /// 法杖的攻击距离 + /// + public int StaffAttackRange { get; set; } = 3; + + /// + /// 长柄武器的攻击距离 + /// + public int PolearmAttackRange { get; set; } = 2; + + /// + /// 拳套的攻击距离 + /// + public int GauntletAttackRange { get; set; } = 1; + + /// + /// 暗器的攻击距离 + /// + public int HiddenWeaponAttackRange { get; set; } = 4; + + /// + /// 核心角色的移动距离 + /// + public int RoleMOV_Core { get; set; } = 3; + + /// + /// 先锋角色的移动距离 + /// + public int RoleMOV_Vanguard { get; set; } = 6; + + /// + /// 近卫角色的移动距离 + /// + public int RoleMOV_Guardian { get; set; } = 5; + + /// + /// 支援角色的移动距离 + /// + public int RoleMOV_Support { get; set; } = 4; + + /// + /// 治疗角色的移动距离 + /// + public int RoleMOV_Medic { get; set; } = 3; + /// /// 应用此游戏平衡常数给实体 /// diff --git a/Model/GamingQueue.cs b/Model/GamingQueue.cs index d8df0f2..1747375 100644 --- a/Model/GamingQueue.cs +++ b/Model/GamingQueue.cs @@ -1,4 +1,5 @@ using Milimoe.FunGame.Core.Api.Utility; +using Milimoe.FunGame.Core.Controller; using Milimoe.FunGame.Core.Entity; using Milimoe.FunGame.Core.Interface.Base; using Milimoe.FunGame.Core.Interface.Entity; @@ -753,6 +754,8 @@ namespace Milimoe.FunGame.Core.Model } } } + + _eliminated.Remove(character); } // 减少复活倒计时 @@ -812,11 +815,11 @@ namespace Milimoe.FunGame.Core.Model // 队友列表 List allTeammates = GetTeammates(character); - List teammates = [.. allTeammates.Where(_queue.Contains)]; + List selecableTeammates = [.. allTeammates.Where(_queue.Contains)]; // 敌人列表 - List allEnemys = [.. _allCharacters.Where(c => c != character && !teammates.Contains(c))]; - List enemys = [.. allEnemys.Where(c => _queue.Contains(c) && !c.IsUnselectable)]; + List allEnemys = [.. _allCharacters.Where(c => c != character && !allTeammates.Contains(c))]; + List selectableEnemys = [.. allEnemys.Where(c => _queue.Contains(c) && !c.IsUnselectable)]; // 技能列表 List skills = [.. character.Skills.Where(s => s.Level > 0 && s.SkillType != SkillType.Passive && s.Enable && !s.IsInEffect && s.CurrentCD == 0 && @@ -828,7 +831,7 @@ namespace Milimoe.FunGame.Core.Model // 回合开始事件,允许事件返回 false 接管回合操作 // 如果事件全程接管回合操作,需要注意触发特效 - if (!await OnTurnStartAsync(character, enemys, teammates, skills, items)) + if (!await OnTurnStartAsync(character, selectableEnemys, selecableTeammates, skills, items)) { _isInRound = false; return _isGameEnd; @@ -836,13 +839,13 @@ namespace Milimoe.FunGame.Core.Model foreach (Skill skillTurnStart in skills) { - skillTurnStart.OnTurnStart(character, enemys, teammates, skills, items); + skillTurnStart.OnTurnStart(character, selectableEnemys, selecableTeammates, skills, items); } List effects = [.. character.Effects.Where(e => e.IsInEffect)]; foreach (Effect effect in effects) { - effect.OnTurnStart(character, enemys, teammates, skills, items); + effect.OnTurnStart(character, selectableEnemys, selecableTeammates, skills, items); } // 此变量用于在取消选择时,能够重新行动 @@ -852,24 +855,33 @@ namespace Milimoe.FunGame.Core.Model // 此变量控制角色移动后可以继续选择其他的行动 bool moved = false; - Grid? currentGrid = null; + // AI 决策控制器,适用于启用战棋地图的情况 + AIController? ai = null; + + // 角色的起始地点,确保角色该回合移动的范围不超过 MOV + Grid? startGrid = null; + List canMoveGrids = []; + // 并且要筛选最远可选取角色 + List canAttackGridsByStartGrid = []; + List canCastGridsByStartGrid = []; if (_map != null) { - currentGrid = _map.GetCharacterCurrentGrid(character); - } + startGrid = _map.GetCharacterCurrentGrid(character); - // 行动开始前,可以修改可选取的角色列表 - Dictionary continuousKillingTemp = new(_continuousKilling); - Dictionary earnedMoneyTemp = new(_earnedMoney); - effects = [.. character.Effects.Where(e => e.IsInEffect)]; - foreach (Effect effect in effects) - { - effect.AlterSelectListBeforeAction(character, enemys, teammates, skills, continuousKillingTemp, earnedMoneyTemp); - } + if (startGrid != null) + { + canMoveGrids = _map.GetGridsByRange(startGrid, character.MOV, false); + canAttackGridsByStartGrid = _map.GetGridsByRange(startGrid, character.ATR, true); + Skill[] canCastSkills = [.. skills, .. items.Select(i => i.Skills.Active!)]; + foreach (Skill skill in canCastSkills) + { + canCastGridsByStartGrid.AddRange(_map.GetGridsByRange(startGrid, skill.CastRange, true)); + } + } - // 这里筛掉重复角色 - enemys = [.. enemys.Distinct()]; - teammates = [.. teammates.Distinct()]; + allEnemys = [.. allEnemys.Where(canAttackGridsByStartGrid.Union(canCastGridsByStartGrid).SelectMany(g => g.Characters).Contains)]; + allTeammates = [.. allTeammates.Where(canAttackGridsByStartGrid.Union(canCastGridsByStartGrid).SelectMany(g => g.Characters).Contains)]; + } // 作出了什么行动 CharacterActionType type = CharacterActionType.None; @@ -880,10 +892,62 @@ namespace Milimoe.FunGame.Core.Model bool isAI = CharactersInAI.Contains(character); while (!decided && (!isAI || cancelTimes > 0)) { + // 根据当前位置,更新可选取角色列表 + Grid? realGrid = null; + List canAttackGrids = []; + List canCastGrids = []; + List willMoveGridWithSkill = []; + List enemys = []; + List teammates = []; + if (_map != null) + { + if (isAI) + { + ai ??= new(this, _map); + } + + realGrid = _map.GetCharacterCurrentGrid(character); + + if (realGrid != null) + { + canAttackGrids = _map.GetGridsByRange(realGrid, character.ATR, true); + Skill[] canCastSkills = [.. skills, .. items.Select(i => i.Skills.Active!)]; + foreach (Skill skill in canCastSkills) + { + canCastGrids.AddRange(_map.GetGridsByRange(realGrid, skill.CastRange, true)); + } + } + + enemys = [.. selectableEnemys.Where(canAttackGrids.Union(canCastGrids).SelectMany(g => g.Characters).Contains)]; + teammates = [.. selecableTeammates.Where(canAttackGrids.Union(canCastGrids).SelectMany(g => g.Characters).Contains)]; + willMoveGridWithSkill = [.. canMoveGrids.Where(g => canAttackGrids.Union(canCastGrids).Contains(g))]; + } + else + { + enemys = selectableEnemys; + teammates = selecableTeammates; + } + + // AI 决策结果(适用于启用战棋地图的情况) + AIDecision? aiDecision = null; + + // 行动开始前,可以修改可选取的角色列表 + Dictionary continuousKillingTemp = new(_continuousKilling); + Dictionary earnedMoneyTemp = new(_earnedMoney); + effects = [.. character.Effects.Where(e => e.IsInEffect)]; + foreach (Effect effect in effects) + { + effect.AlterSelectListBeforeAction(character, enemys, teammates, skills, continuousKillingTemp, earnedMoneyTemp); + } + + // 这里筛掉重复角色 + enemys = [.. enemys.Distinct()]; + teammates = [.. teammates.Distinct()]; + if (moved) moved = false; else cancelTimes--; type = CharacterActionType.None; - + // 是否能使用物品和释放技能 bool canUseItem = items.Count > 0; bool canCastSkill = skills.Count > 0; @@ -996,8 +1060,17 @@ namespace Milimoe.FunGame.Core.Model } } - // 模组可以通过此事件来决定角色的行动 - type = await OnDecideActionAsync(character, enemys, teammates, skills, items); + // 启用战棋地图时的专属 AI 决策方法 + if (isAI && ai != null && startGrid != null) + { + aiDecision = await ai.DecideAIActionAsync(character, startGrid, canMoveGrids, skills, items, allEnemys, allTeammates); + type = aiDecision.ActionType; + } + else + { + // 模组可以通过此事件来决定角色的行动 + type = await OnDecideActionAsync(character, enemys, teammates, skills, items); + } // 若事件未完成决策,则将通过概率对角色进行自动化决策 if (type == CharacterActionType.None) { @@ -1021,7 +1094,34 @@ namespace Milimoe.FunGame.Core.Model } } - if (type == CharacterActionType.NormalAttack) + if (aiDecision != null && aiDecision.ActionType != CharacterActionType.Move && aiDecision.TargetMoveGrid != null) + { + // 不是纯粹移动的情况,需要手动移动 + moved = await CharacterMoveAsync(character, aiDecision.TargetMoveGrid, startGrid); + } + + if (type == CharacterActionType.Move) + { + if (_map != null) + { + Grid target; + if (aiDecision != null && aiDecision.TargetMoveGrid != null) + { + target = aiDecision.TargetMoveGrid; + } + else + { + target = await SelectTargetGridAsync(character, enemys, teammates, _map, canMoveGrids); + } + moved = await CharacterMoveAsync(character, target, startGrid); + } + if (isAI && aiDecision != null && cancelTimes == 0) + { + // 取消 AI 的移动 + type = CharacterActionType.EndTurn; + } + } + else if (type == CharacterActionType.NormalAttack) { if (!forceAction && (character.CharacterState == CharacterState.NotActionable || character.CharacterState == CharacterState.ActionRestricted || @@ -1033,7 +1133,22 @@ namespace Milimoe.FunGame.Core.Model else { // 使用普通攻击逻辑 - List targets = await SelectTargetsAsync(character, character.NormalAttack, enemys, teammates); + List targets; + if (aiDecision != null) + { + targets = aiDecision.Targets; + } + else + { + List attackRange = []; + if (_map != null && realGrid != null) + { + attackRange = _map.GetGridsByRange(realGrid, character.ATR, true); + enemys = [.. enemys.Where(attackRange.SelectMany(g => g.Characters).Contains)]; + teammates = [.. teammates.Where(attackRange.SelectMany(g => g.Characters).Contains)]; + } + targets = await SelectTargetsAsync(character, character.NormalAttack, enemys, teammates, attackRange); + } if (targets.Count > 0) { LastRound.Targets = [.. targets]; @@ -1063,7 +1178,15 @@ namespace Milimoe.FunGame.Core.Model else { // 预使用技能,即开始吟唱逻辑 - Skill? skill = await OnSelectSkillAsync(character, skills); + Skill? skill; + if (aiDecision != null && aiDecision.SkillToUse is Skill s) + { + skill = s; + } + else + { + skill = await OnSelectSkillAsync(character, skills); + } if (skill is null && CharactersInAI.Contains(character) && skills.Count > 0) { skill = skills[Random.Shared.Next(skills.Count)]; @@ -1073,7 +1196,22 @@ namespace Milimoe.FunGame.Core.Model // 吟唱前需要先选取目标 if (skill.SkillType == SkillType.Magic) { - List targets = await SelectTargetsAsync(character, skill, enemys, teammates); + List targets; + if (aiDecision != null) + { + targets = aiDecision.Targets; + } + else + { + List castRange = []; + if (_map != null && realGrid != null) + { + castRange = _map.GetGridsByRange(realGrid, skill.CastRange, true); + enemys = [.. enemys.Where(castRange.SelectMany(g => g.Characters).Contains)]; + teammates = [.. teammates.Where(castRange.SelectMany(g => g.Characters).Contains)]; + } + targets = await SelectTargetsAsync(character, skill, enemys, teammates, castRange); + } if (targets.Count > 0) { // 免疫检定 @@ -1100,7 +1238,22 @@ namespace Milimoe.FunGame.Core.Model // 只有魔法需要吟唱,战技和爆发技直接释放 if (CheckCanCast(character, skill, out double cost)) { - List targets = await SelectTargetsAsync(character, skill, enemys, teammates); + List targets; + if (aiDecision != null) + { + targets = aiDecision.Targets; + } + else + { + List castRange = []; + if (_map != null && realGrid != null) + { + castRange = _map.GetGridsByRange(realGrid, skill.CastRange, true); + enemys = [.. enemys.Where(castRange.SelectMany(g => g.Characters).Contains)]; + teammates = [.. teammates.Where(castRange.SelectMany(g => g.Characters).Contains)]; + } + targets = await SelectTargetsAsync(character, skill, enemys, teammates, castRange); + } if (targets.Count > 0) { // 免疫检定 @@ -1211,7 +1364,8 @@ namespace Milimoe.FunGame.Core.Model if (CheckCanCast(character, skill, out double cost)) { // 预释放的爆发技不可取消 - List targets = await SelectTargetsAsync(character, skill, enemys, teammates); + List castRange = _map != null && realGrid != null ? _map.GetGridsByRange(realGrid, skill.CastRange, true) : []; + List targets = await SelectTargetsAsync(character, skill, enemys, teammates, castRange); // 免疫检定 await CheckSkilledImmuneAsync(character, targets, skill); LastRound.Targets = [.. targets]; @@ -1246,7 +1400,15 @@ namespace Milimoe.FunGame.Core.Model else if (type == CharacterActionType.UseItem) { // 使用物品逻辑 - Item? item = await OnSelectItemAsync(character, items); + Item? item; + if (aiDecision != null && aiDecision.ItemToUse != null) + { + item = aiDecision.ItemToUse; + } + else + { + item = await OnSelectItemAsync(character, items); + } if (item is null && CharactersInAI.Contains(character) && items.Count > 0) { // AI 控制下随机选取一个物品 @@ -1255,7 +1417,14 @@ namespace Milimoe.FunGame.Core.Model if (item != null && item.Skills.Active != null) { Skill skill = item.Skills.Active; - if (await UseItemAsync(item, character, enemys, teammates)) + List castRange = []; + if (_map != null && realGrid != null) + { + castRange = _map.GetGridsByRange(realGrid, skill.CastRange, true); + enemys = [.. enemys.Where(castRange.SelectMany(g => g.Characters).Contains)]; + teammates = [.. teammates.Where(castRange.SelectMany(g => g.Characters).Contains)]; + } + if (await UseItemAsync(item, character, enemys, teammates, castRange, aiDecision?.Targets)) { decided = true; LastRound.Item = item; @@ -1282,20 +1451,6 @@ namespace Milimoe.FunGame.Core.Model WriteLine($"[ {character} ] 结束了回合!"); await OnCharacterDoNothingAsync(character); } - else if (type == CharacterActionType.Move) - { - if (_map != null) - { - Grid target = await SelectTargetGridAsync(character, enemys, teammates, _map); - if (target.Id != -1) - { - int steps = _map.CharacterMove(character, currentGrid, target); - moved = true; - WriteLine($"[ {character} ] 移动了 {steps} 步!"); - await OnCharacterMoveAsync(character, target); - } - } - } else { if (baseTime == 0) baseTime += 8; @@ -2213,8 +2368,10 @@ namespace Milimoe.FunGame.Core.Model /// /// /// + /// + /// /// - public async Task UseItemAsync(Item item, Character character, List enemys, List teammates) + public async Task UseItemAsync(Item item, Character character, List enemys, List teammates, List castRange, List? desiredTargets = null) { if (CheckCanCast(character, item, out double costMP, out double costEP)) { @@ -2222,7 +2379,15 @@ namespace Milimoe.FunGame.Core.Model if (skill != null) { skill.GamingQueue = this; - List targets = await SelectTargetsAsync(character, skill, enemys, teammates); + List targets; + if (desiredTargets != null) + { + targets = desiredTargets; + } + else + { + targets = await SelectTargetsAsync(character, skill, enemys, teammates, castRange); + } if (targets.Count > 0) { // 免疫检定 @@ -2277,6 +2442,28 @@ namespace Milimoe.FunGame.Core.Model return false; } + /// + /// 角色移动实际逻辑 + /// + /// + /// + /// + /// + public async Task CharacterMoveAsync(Character character, Grid target, Grid? startGrid) + { + if (target.Id != -1) + { + int steps = _map?.CharacterMove(character, startGrid, target) ?? -1; + if (steps > 0) + { + WriteLine($"[ {character} ] 移动了 {steps} 步!"); + await OnCharacterMoveAsync(character, target); + return true; + } + } + return false; + } + /// /// 通过概率计算角色要干嘛 /// @@ -2329,25 +2516,25 @@ namespace Milimoe.FunGame.Core.Model /// /// /// + /// /// - public async Task SelectTargetGridAsync(Character character, List enemys, List teammates, GameMap map) + public async Task SelectTargetGridAsync(Character character, List enemys, List teammates, GameMap map, List moveRange) { List effects = [.. character.Effects.Where(e => e.IsInEffect)]; foreach (Effect effect in effects) { - effect.BeforeSelectTargetGrid(character, enemys, teammates, map); + effect.BeforeSelectTargetGrid(character, enemys, teammates, map, moveRange); } - Grid target = await OnSelectTargetGridAsync(character, enemys, teammates, map); + Grid target = await OnSelectTargetGridAsync(character, enemys, teammates, map, moveRange); if (target.Id != -1) { return target; } else if (target.Id == -2 && map.Characters.TryGetValue(character, out Grid? current) && current != null) { - List grids = map.GetGridsByRange(current, character.MOV); - if (grids.Count > 0) + if (moveRange.Count > 0) { - return grids[Random.Shared.Next(grids.Count)]; + return moveRange[Random.Shared.Next(moveRange.Count)]; } } return Grid.Empty; @@ -2360,15 +2547,16 @@ namespace Milimoe.FunGame.Core.Model /// /// /// + /// /// - public async Task> SelectTargetsAsync(Character caster, Skill skill, List enemys, List teammates) + public async Task> SelectTargetsAsync(Character caster, Skill skill, List enemys, List teammates, List castRange) { List effects = [.. caster.Effects.Where(e => e.IsInEffect)]; foreach (Effect effect in effects) { effect.AlterSelectListBeforeSelection(caster, skill, enemys, teammates); } - List targets = await OnSelectSkillTargetsAsync(caster, skill, enemys, teammates); + List targets = await OnSelectSkillTargetsAsync(caster, skill, enemys, teammates, castRange); if (targets.Count == 0 && CharactersInAI.Contains(caster)) { targets = skill.SelectTargets(caster, enemys, teammates); @@ -2383,15 +2571,16 @@ namespace Milimoe.FunGame.Core.Model /// /// /// + /// /// - public async Task> SelectTargetsAsync(Character character, NormalAttack attack, List enemys, List teammates) + public async Task> SelectTargetsAsync(Character character, NormalAttack attack, List enemys, List teammates, List attackRange) { List effects = [.. character.Effects.Where(e => e.IsInEffect)]; foreach (Effect effect in effects) { effect.AlterSelectListBeforeSelection(character, attack, enemys, teammates); } - List targets = await OnSelectNormalAttackTargetsAsync(character, attack, enemys, teammates); + List targets = await OnSelectNormalAttackTargetsAsync(character, attack, enemys, teammates, attackRange); if (targets.Count == 0 && CharactersInAI.Contains(character)) { targets = character.NormalAttack.SelectTargets(character, enemys, teammates); @@ -3359,7 +3548,7 @@ namespace Milimoe.FunGame.Core.Model return await (SelectItem?.Invoke(this, character, items) ?? Task.FromResult(null)); } - public delegate Task SelectTargetGridEventHandler(GamingQueue queue, Character character, List enemys, List teammates, GameMap map); + public delegate Task SelectTargetGridEventHandler(GamingQueue queue, Character character, List enemys, List teammates, GameMap map, List moveRange); /// /// 选取移动目标事件 /// @@ -3371,13 +3560,14 @@ namespace Milimoe.FunGame.Core.Model /// /// /// + /// /// - protected async Task OnSelectTargetGridAsync(Character character, List enemys, List teammates, GameMap map) + protected async Task OnSelectTargetGridAsync(Character character, List enemys, List teammates, GameMap map, List moveRange) { - return await (SelectTargetGrid?.Invoke(this, character, enemys, teammates, map) ?? Task.FromResult(Grid.Empty)); + return await (SelectTargetGrid?.Invoke(this, character, enemys, teammates, map, moveRange) ?? Task.FromResult(Grid.Empty)); } - public delegate Task> SelectSkillTargetsEventHandler(GamingQueue queue, Character caster, Skill skill, List enemys, List teammates); + public delegate Task> SelectSkillTargetsEventHandler(GamingQueue queue, Character caster, Skill skill, List enemys, List teammates, List castRange); /// /// 选取技能目标事件 /// @@ -3389,13 +3579,14 @@ namespace Milimoe.FunGame.Core.Model /// /// /// + /// /// - protected async Task> OnSelectSkillTargetsAsync(Character caster, Skill skill, List enemys, List teammates) + protected async Task> OnSelectSkillTargetsAsync(Character caster, Skill skill, List enemys, List teammates, List castRange) { - return await (SelectSkillTargets?.Invoke(this, caster, skill, enemys, teammates) ?? Task.FromResult(new List())); + return await (SelectSkillTargets?.Invoke(this, caster, skill, enemys, teammates, castRange) ?? Task.FromResult(new List())); } - public delegate Task> SelectNormalAttackTargetsEventHandler(GamingQueue queue, Character character, NormalAttack attack, List enemys, List teammates); + public delegate Task> SelectNormalAttackTargetsEventHandler(GamingQueue queue, Character character, NormalAttack attack, List enemys, List teammates, List attackRange); /// /// 选取普通攻击目标事件 /// @@ -3407,10 +3598,11 @@ namespace Milimoe.FunGame.Core.Model /// /// /// + /// /// - protected async Task> OnSelectNormalAttackTargetsAsync(Character character, NormalAttack attack, List enemys, List teammates) + protected async Task> OnSelectNormalAttackTargetsAsync(Character character, NormalAttack attack, List enemys, List teammates, List attackRange) { - return await (SelectNormalAttackTargets?.Invoke(this, character, attack, enemys, teammates) ?? Task.FromResult(new List())); + return await (SelectNormalAttackTargets?.Invoke(this, character, attack, enemys, teammates, attackRange) ?? Task.FromResult(new List())); } public delegate Task InterruptCastingEventHandler(GamingQueue queue, Character cast, Skill? skill, Character interrupter); diff --git a/Model/TeamGamingQueue.cs b/Model/TeamGamingQueue.cs index 1ee7079..17b0acd 100644 --- a/Model/TeamGamingQueue.cs +++ b/Model/TeamGamingQueue.cs @@ -147,7 +147,7 @@ namespace Milimoe.FunGame.Core.Model { string[] teamActive = [.. Teams.OrderByDescending(kv => kv.Value.Score).Select(kv => { - int activeCount = kv.Value.GetActiveCharacters(this).Count; + int activeCount = kv.Value.GetActiveCharacters().Count; if (kv.Value == killTeam) { activeCount += 1; @@ -159,7 +159,7 @@ namespace Milimoe.FunGame.Core.Model if (deathTeam != null) { - List remain = deathTeam.GetActiveCharacters(this); + List remain = deathTeam.GetActiveCharacters(); int remainCount = remain.Count; if (remainCount == 0) { @@ -175,7 +175,7 @@ namespace Milimoe.FunGame.Core.Model if (killTeam != null) { - List actives = killTeam.GetActiveCharacters(this); + List actives = killTeam.GetActiveCharacters(); int remainCount = actives.Count; if (remainCount > 0 && MaxRespawnTimes == 0) {