支持战棋地图玩法 (#142)

This commit is contained in:
milimoe 2025-09-13 15:10:11 +08:00 committed by GitHub
parent 09eea71cb6
commit 2c39f46dd9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1067 additions and 124 deletions

322
Controller/AIController.cs Normal file
View File

@ -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;
/// <summary>
/// AI的核心决策方法根据当前游戏状态为角色选择最佳行动。
/// </summary>
/// <param name="character">当前行动的AI角色。</param>
/// <param name="startGrid">角色的起始格子。</param>
/// <param name="allPossibleMoveGrids">从起始格子可达的所有移动格子(包括起始格子本身)。</param>
/// <param name="availableSkills">角色所有可用的技能已过滤CD和EP/MP。</param>
/// <param name="availableItems">角色所有可用的物品已过滤CD和EP/MP。</param>
/// <param name="allEnemysInGame">场上所有敌人。</param>
/// <param name="allTeammatesInGame">场上所有队友。</param>
/// <returns>包含最佳行动的AIDecision对象。</returns>
public async Task<AIDecision> DecideAIActionAsync(Character character, Grid startGrid, List<Grid> allPossibleMoveGrids,
List<Skill> availableSkills, List<Item> availableItems, List<Character> allEnemysInGame, List<Character> 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<Grid> normalAttackReachableGrids = _map.GetGridsByRange(potentialMoveGrid, character.ATR, true);
List<Character> normalAttackReachableEnemys = [.. allEnemysInGame
.Where(c => normalAttackReachableGrids.SelectMany(g => g.Characters).Contains(c) && !c.IsUnselectable)
.Distinct()];
List<Character> normalAttackReachableTeammates = [.. allTeammatesInGame
.Where(c => normalAttackReachableGrids.SelectMany(g => g.Characters).Contains(c))
.Distinct()];
if (normalAttackReachableEnemys.Count > 0)
{
// 将筛选后的目标列表传递给 SelectTargets
List<Character> 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<Grid> skillReachableGrids = _map.GetGridsByRange(potentialMoveGrid, skill.CastRange, true);
List<Character> skillReachableEnemys = [.. allEnemysInGame
.Where(c => skillReachableGrids.SelectMany(g => g.Characters).Contains(c) && !c.IsUnselectable)
.Distinct()];
List<Character> skillReachableTeammates = [.. allTeammatesInGame
.Where(c => skillReachableGrids.SelectMany(g => g.Characters).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;
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<Grid> itemSkillReachableGrids = _map.GetGridsByRange(potentialMoveGrid, itemSkill.CastRange, true);
List<Character> itemSkillReachableEnemys = [.. allEnemysInGame
.Where(c => itemSkillReachableGrids.SelectMany(g => g.Characters).Contains(c) && !c.IsUnselectable)
.Distinct()];
List<Character> itemSkillReachableTeammates = [.. allTeammatesInGame
.Where(c => itemSkillReachableGrids.SelectMany(g => g.Characters).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;
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<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)
.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;
}
/// <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)
{
List<Character> targets = skill.GetSelectableTargets(character, enemys, teammates);
int count = skill.RealCanSelectTargetCount(enemys, teammates);
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)
{
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;
}
/// <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)
{
double score = 0;
score += targets.Sum(t => CalculateTargetValue(t, skill));
//score -= cost * 5;
//score -= skill.RealCD * 2;
//score -= skill.HardnessTime * 2;
return score;
}
/// <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)
{
double score = Random.Shared.Next(1000);
return score;
}
/// <summary>
/// 辅助函数:计算单个目标在某个技能下的价值
/// </summary>
/// <param name="target"></param>
/// <param name="skill"></param>
/// <returns></returns>
private static double CalculateTargetValue(Character target, ISkill skill)
{
double value = Random.Shared.Next(1000);
return value;
}
}
}

View File

@ -219,7 +219,7 @@ namespace Milimoe.FunGame.Core.Entity
/// <summary> /// <summary>
/// 最大生命值 = 基础生命值 + 额外生命值 + 额外生命值2 + 额外生命值3 /// 最大生命值 = 基础生命值 + 额外生命值 + 额外生命值2 + 额外生命值3
/// </summary> /// </summary>
public double MaxHP => BaseHP + ExHP + ExHP2 + ExHP3; public double MaxHP => Math.Max(1, BaseHP + ExHP + ExHP2 + ExHP3);
/// <summary> /// <summary>
/// 当前生命值 [ 战斗相关 ] /// 当前生命值 [ 战斗相关 ]
@ -238,6 +238,12 @@ namespace Milimoe.FunGame.Core.Entity
} }
} }
/// <summary>
/// 是否有魔法值 [ 初始设定 ]
/// </summary>
[InitRequired]
public bool HasMP { get; set; } = true;
/// <summary> /// <summary>
/// 初始魔法值 [ 初始设定 ] /// 初始魔法值 [ 初始设定 ]
/// </summary> /// </summary>
@ -272,7 +278,7 @@ namespace Milimoe.FunGame.Core.Entity
/// <summary> /// <summary>
/// 最大魔法值 = 基础魔法值 + 额外魔法值 + 额外魔法值2 + 额外魔法值3 /// 最大魔法值 = 基础魔法值 + 额外魔法值 + 额外魔法值2 + 额外魔法值3
/// </summary> /// </summary>
public double MaxMP => BaseMP + ExMP + ExMP2 + ExMP3; public double MaxMP => Math.Max(1, BaseMP + ExMP + ExMP2 + ExMP3);
/// <summary> /// <summary>
/// 当前魔法值 [ 战斗相关 ] /// 当前魔法值 [ 战斗相关 ]
@ -749,16 +755,68 @@ namespace Milimoe.FunGame.Core.Entity
public double ExCDR { get; set; } = 0; public double ExCDR { get; set; } = 0;
/// <summary> /// <summary>
/// 攻击距离 [ 与技能和物品相关 ] [ 单位:格(半径) ] /// 攻击距离 [ 与武器相关 ] [ 单位:格(半径) ]
/// </summary> /// </summary>
[InitOptional] public int ATR
public int ATR { get; set; } = 1; {
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);
}
}
/// <summary>
/// 额外攻击距离 [ 与技能和物品相关 ] [ 单位:格(半径) ]
/// </summary>
public int ExATR { get; set; } = 0;
/// <summary>
/// 行动力/可移动距离 [ 与第一定位相关 ] [ 单位:格(半径) ]
/// </summary>
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);
}
}
/// <summary> /// <summary>
/// 行动力/可移动距离 [ 与技能和物品相关 ] [ 单位:格(半径) ] /// 行动力/可移动距离 [ 与技能和物品相关 ] [ 单位:格(半径) ]
/// </summary> /// </summary>
[InitOptional] public int ExMOV { get; set; } = 0;
public int MOV { get; set; } = 5;
/// <summary> /// <summary>
/// 暴击率(%) = [ 与敏捷相关 ] + 额外暴击率(%) /// 暴击率(%) = [ 与敏捷相关 ] + 额外暴击率(%)
@ -1388,7 +1446,7 @@ namespace Milimoe.FunGame.Core.Entity
/// 获取角色的详细信息 /// 获取角色的详细信息
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
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(); StringBuilder builder = new();
@ -1434,6 +1492,12 @@ namespace Milimoe.FunGame.Core.Entity
builder.AppendLine($"魔法消耗减少:{INT * GameplayEquilibriumConstant.INTtoCastMPReduce * 100:0.##}%"); builder.AppendLine($"魔法消耗减少:{INT * GameplayEquilibriumConstant.INTtoCastMPReduce * 100:0.##}%");
builder.AppendLine($"能量消耗减少:{INT * GameplayEquilibriumConstant.INTtoCastEPReduce * 100:0.##}%"); builder.AppendLine($"能量消耗减少:{INT * GameplayEquilibriumConstant.INTtoCastEPReduce * 100:0.##}%");
if (showMapRelated)
{
builder.AppendLine($"移动距离:{MOV}");
builder.AppendLine($"攻击距离:{ATR}");
}
GetStatusInfo(builder); GetStatusInfo(builder);
builder.AppendLine("== 普通攻击 =="); builder.AppendLine("== 普通攻击 ==");
@ -1475,7 +1539,7 @@ namespace Milimoe.FunGame.Core.Entity
/// 获取角色的简略信息 /// 获取角色的简略信息
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
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(); 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($"生命回复:{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.##}]" : "")); 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 (!showBasicOnly)
{ {
GetStatusInfo(builder); GetStatusInfo(builder);
@ -1685,7 +1755,7 @@ namespace Milimoe.FunGame.Core.Entity
/// 获取角色的物品信息 /// 获取角色的物品信息
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
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(); StringBuilder builder = new();
@ -1731,6 +1801,12 @@ namespace Milimoe.FunGame.Core.Entity
builder.AppendLine($"魔法消耗减少:{INT * GameplayEquilibriumConstant.INTtoCastMPReduce * 100:0.##}%"); builder.AppendLine($"魔法消耗减少:{INT * GameplayEquilibriumConstant.INTtoCastMPReduce * 100:0.##}%");
builder.AppendLine($"能量消耗减少:{INT * GameplayEquilibriumConstant.INTtoCastEPReduce * 100:0.##}%"); builder.AppendLine($"能量消耗减少:{INT * GameplayEquilibriumConstant.INTtoCastEPReduce * 100:0.##}%");
if (showMapRelated)
{
builder.AppendLine($"移动距离:{MOV}");
builder.AppendLine($"攻击距离:{ATR}");
}
if (EquipSlot.Any()) if (EquipSlot.Any())
{ {
builder.AppendLine(GetEquipSlotInfo().Trim()); builder.AppendLine(GetEquipSlotInfo().Trim());
@ -2022,8 +2098,8 @@ namespace Milimoe.FunGame.Core.Entity
c.MDF = MDF.Copy(); c.MDF = MDF.Copy();
c.Lifesteal = Lifesteal; c.Lifesteal = Lifesteal;
c.Shield = Shield.Copy(); c.Shield = Shield.Copy();
c.ATR = ATR; c.ExATR = ExATR;
c.MOV = MOV; c.ExMOV = ExMOV;
c.MagicType = MagicType; c.MagicType = MagicType;
c.ImmuneType = ImmuneType; c.ImmuneType = ImmuneType;
} }
@ -2132,8 +2208,8 @@ namespace Milimoe.FunGame.Core.Entity
ExActionCoefficient = c.ExActionCoefficient; ExActionCoefficient = c.ExActionCoefficient;
ExAccelerationCoefficient = c.ExAccelerationCoefficient; ExAccelerationCoefficient = c.ExAccelerationCoefficient;
ExCDR = c.ExCDR; ExCDR = c.ExCDR;
ATR = c.ATR; ExATR = c.ExATR;
MOV = c.MOV; ExMOV = c.ExMOV;
ExCritRate = c.ExCritRate; ExCritRate = c.ExCritRate;
ExCritDMG = c.ExCritDMG; ExCritDMG = c.ExCritDMG;
ExEvadeRate = c.ExEvadeRate; ExEvadeRate = c.ExEvadeRate;

View File

@ -47,7 +47,7 @@ namespace Milimoe.FunGame.Core.Entity
/// 获取单位的详细信息 /// 获取单位的详细信息
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
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(); StringBuilder builder = new();
@ -78,6 +78,12 @@ namespace Milimoe.FunGame.Core.Entity
builder.AppendLine($"物理穿透:{PhysicalPenetration * 100:0.##}%"); builder.AppendLine($"物理穿透:{PhysicalPenetration * 100:0.##}%");
builder.AppendLine($"魔法穿透:{MagicalPenetration * 100:0.##}%"); builder.AppendLine($"魔法穿透:{MagicalPenetration * 100:0.##}%");
if (showMapRelated)
{
builder.AppendLine($"移动距离:{MOV}");
builder.AppendLine($"攻击距离:{ATR}");
}
if (CharacterState != CharacterState.Actionable) if (CharacterState != CharacterState.Actionable)
{ {
builder.AppendLine(CharacterSet.GetCharacterState(CharacterState)); builder.AppendLine(CharacterSet.GetCharacterState(CharacterState));
@ -174,7 +180,7 @@ namespace Milimoe.FunGame.Core.Entity
/// 获取单位的简略信息 /// 获取单位的简略信息
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
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(); 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($"生命回复:{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.##}]" : "")); 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 (!showBasicOnly)
{ {
if (CharacterState != CharacterState.Actionable) if (CharacterState != CharacterState.Actionable)
@ -408,7 +420,7 @@ namespace Milimoe.FunGame.Core.Entity
/// 获取单位的物品信息 /// 获取单位的物品信息
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
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(); StringBuilder builder = new();
@ -439,6 +451,12 @@ namespace Milimoe.FunGame.Core.Entity
builder.AppendLine($"物理穿透:{PhysicalPenetration * 100:0.##}%"); builder.AppendLine($"物理穿透:{PhysicalPenetration * 100:0.##}%");
builder.AppendLine($"魔法穿透:{MagicalPenetration * 100:0.##}%"); builder.AppendLine($"魔法穿透:{MagicalPenetration * 100:0.##}%");
if (showMapRelated)
{
builder.AppendLine($"移动距离:{MOV}");
builder.AppendLine($"攻击距离:{ATR}");
}
if (EquipSlot.Any()) if (EquipSlot.Any())
{ {
builder.AppendLine("== 装备栏 =="); builder.AppendLine("== 装备栏 ==");

View File

@ -1,7 +1,9 @@
using System.Text; using System.Collections.Generic;
using System.Text;
using Milimoe.FunGame.Core.Api.Utility; using Milimoe.FunGame.Core.Api.Utility;
using Milimoe.FunGame.Core.Interface.Base; using Milimoe.FunGame.Core.Interface.Base;
using Milimoe.FunGame.Core.Interface.Entity; using Milimoe.FunGame.Core.Interface.Entity;
using Milimoe.FunGame.Core.Library.Common.Addon;
using Milimoe.FunGame.Core.Library.Constant; using Milimoe.FunGame.Core.Library.Constant;
namespace Milimoe.FunGame.Core.Entity namespace Milimoe.FunGame.Core.Entity
@ -310,7 +312,13 @@ namespace Milimoe.FunGame.Core.Entity
} }
if (result && Skills.Active != null) if (result && Skills.Active != null)
{ {
used = await queue.UseItemAsync(this, character, enemys, teammates); List<Grid> 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) if (used)
{ {

View File

@ -1,4 +1,5 @@
using System.Text; using System.Collections.Generic;
using System.Text;
using Milimoe.FunGame.Core.Api.Utility; using Milimoe.FunGame.Core.Api.Utility;
using Milimoe.FunGame.Core.Interface.Base; using Milimoe.FunGame.Core.Interface.Base;
using Milimoe.FunGame.Core.Interface.Entity; using Milimoe.FunGame.Core.Interface.Entity;
@ -560,7 +561,8 @@ namespace Milimoe.FunGame.Core.Entity
/// <param name="enemys"></param> /// <param name="enemys"></param>
/// <param name="teammates"></param> /// <param name="teammates"></param>
/// <param name="map"></param> /// <param name="map"></param>
public virtual void BeforeSelectTargetGrid(Character character, List<Character> enemys, List<Character> teammates, GameMap map) /// <param name="moveRange"></param>
public virtual void BeforeSelectTargetGrid(Character character, List<Character> enemys, List<Character> teammates, GameMap map, List<Grid> moveRange)
{ {
} }
@ -1067,6 +1069,19 @@ namespace Milimoe.FunGame.Core.Entity
return GamingQueue?.IsCharacterInAIControlling(character) ?? false; return GamingQueue?.IsCharacterInAIControlling(character) ?? false;
} }
/// <summary>
/// 添加角色应用的特效类型到回合记录中
/// </summary>
/// <param name="character"></param>
/// <param name="types"></param>
public void RecordCharacterApplyEffects(Character character, params List<EffectType> types)
{
if (GamingQueue?.LastRound.ApplyEffects.TryAdd(character, types) ?? false)
{
GamingQueue?.LastRound.ApplyEffects[character].AddRange(types);
}
}
/// <summary> /// <summary>
/// 返回特效详情 /// 返回特效详情
/// </summary> /// </summary>

View File

@ -233,6 +233,11 @@ namespace Milimoe.FunGame.Core.Entity
/// </summary> /// </summary>
public double CurrentCD => 0; public double CurrentCD => 0;
/// <summary>
/// 游戏中的行动顺序表实例,使用时需要判断其是否存在
/// </summary>
public IGamingQueue? GamingQueue { get; set; } = null;
/// <summary> /// <summary>
/// 绑定到特效的普通攻击扩展。键为特效,值为对应的普攻扩展对象。 /// 绑定到特效的普通攻击扩展。键为特效,值为对应的普攻扩展对象。
/// </summary> /// </summary>
@ -450,6 +455,7 @@ namespace Milimoe.FunGame.Core.Entity
builder.AppendLine($"{Name} - 等级 {Level}"); builder.AppendLine($"{Name} - 等级 {Level}");
builder.AppendLine($"描述:{Description}"); builder.AppendLine($"描述:{Description}");
if (GamingQueue?.Map != null) builder.AppendLine($"攻击距离:{Character.ATR}");
builder.AppendLine($"硬直时间:{RealHardnessTime:0.##}{(showOriginal && RealHardnessTime != HardnessTime ? $"{HardnessTime}" : "")}"); builder.AppendLine($"硬直时间:{RealHardnessTime:0.##}{(showOriginal && RealHardnessTime != HardnessTime ? $"{HardnessTime}" : "")}");
return builder.ToString(); return builder.ToString();

View File

@ -44,6 +44,21 @@ namespace Milimoe.FunGame.Core.Entity
CanSelectTeammate = teammate; CanSelectTeammate = teammate;
} }
break; 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": case "count":
if (int.TryParse(args[str].ToString(), out int count) && count > 0) if (int.TryParse(args[str].ToString(), out int count) && count > 0)
{ {
@ -56,6 +71,19 @@ namespace Milimoe.FunGame.Core.Entity
CanSelectTargetRange = range; CanSelectTargetRange = range;
} }
break; 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": case "mpcost":
if (double.TryParse(args[str].ToString(), out double mpcost) && mpcost > 0) if (double.TryParse(args[str].ToString(), out double mpcost) && mpcost > 0)
{ {
@ -69,12 +97,14 @@ namespace Milimoe.FunGame.Core.Entity
} }
break; break;
case "costall": case "costall":
case "costallep":
if (bool.TryParse(args[str].ToString(), out bool costall) && costall) if (bool.TryParse(args[str].ToString(), out bool costall) && costall)
{ {
CostAllEP = costall; CostAllEP = costall;
} }
break; break;
case "mincost": case "mincost":
case "mincostep":
if (double.TryParse(args[str].ToString(), out double mincost) && mincost > 0) if (double.TryParse(args[str].ToString(), out double mincost) && mincost > 0)
{ {
MinCostEP = mincost; MinCostEP = mincost;
@ -87,12 +117,28 @@ namespace Milimoe.FunGame.Core.Entity
} }
break; break;
case "cast": case "cast":
case "casttime":
if (double.TryParse(args[str].ToString(), out double cast) && cast > 0) if (double.TryParse(args[str].ToString(), out double cast) && cast > 0)
{ {
CastTime = cast; CastTime = cast;
} }
break; 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 "ht":
case "hardnesstime":
if (double.TryParse(args[str].ToString(), out double ht) && ht > 0) if (double.TryParse(args[str].ToString(), out double ht) && ht > 0)
{ {
HardnessTime = ht; HardnessTime = ht;

View File

@ -99,7 +99,11 @@ namespace Milimoe.FunGame.Core.Entity
/// 施法距离 [ 单位:格 ] /// 施法距离 [ 单位:格 ]
/// </summary> /// </summary>
[InitOptional] [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);
}
/// <summary> /// <summary>
/// 可选取自身 /// 可选取自身
@ -136,6 +140,24 @@ namespace Milimoe.FunGame.Core.Entity
/// </summary> /// </summary>
public virtual int CanSelectTargetRange { get; set; } = 0; public virtual int CanSelectTargetRange { get; set; } = 0;
/// <summary>
/// 如果为 true表示非指向性技能可以任意选取一个范围<see cref="CanSelectTargetRange"/> = 0 时为单个格子)。<para/>
/// 如果为 false表示必须选取一个角色作为目标当 <see cref="CanSelectTargetRange"/> > 0 时,技能作用范围将根据目标位置覆盖 <see cref="SkillRangeType"/> 形状的区域;= 0 时正常选取目标。
/// </summary>
public virtual bool IsNonDirectional { get; set; } = false;
/// <summary>
/// 作用范围形状<para/>
/// <see cref="SkillRangeType.Diamond"/> - 菱形。默认的曼哈顿距离正方形<para/>
/// <see cref="SkillRangeType.Circle"/> - 圆形。基于欧几里得距离的圆形<para/>
/// <see cref="SkillRangeType.Square"/> - 正方形<para/>
/// <see cref="SkillRangeType.Line"/> - 施法者与目标之前的直线<para/>
/// <see cref="SkillRangeType.LinePass"/> - 施法者与目标所在的直线,贯穿至地图边缘<para/>
/// <see cref="SkillRangeType.Sector"/> - 扇形<para/>
/// 注意,该属性不影响选取目标的范围。选取目标的范围由 <see cref="Library.Common.Addon.GameMap"/> 决定。
/// </summary>
public virtual SkillRangeType SkillRangeType { get; set; } = SkillRangeType.Diamond;
/// <summary> /// <summary>
/// 选取角色的条件 /// 选取角色的条件
/// </summary> /// </summary>
@ -534,6 +556,10 @@ namespace Milimoe.FunGame.Core.Entity
{ {
builder.AppendLine($"{DispelDescription}"); builder.AppendLine($"{DispelDescription}");
} }
if (GamingQueue?.Map != null && SkillType != SkillType.Passive)
{
builder.AppendLine($"施法距离:{(CastAnywhere ? "" : CastRange)}");
}
if (IsActive && (Item?.IsInGameItem ?? true)) if (IsActive && (Item?.IsInGameItem ?? true))
{ {
if (SkillType == SkillType.Item) if (SkillType == SkillType.Item)
@ -661,5 +687,10 @@ namespace Milimoe.FunGame.Core.Entity
/// 等级 /// 等级
/// </summary> /// </summary>
private int _Level = 0; private int _Level = 0;
/// <summary>
/// 施法距离
/// </summary>
private int _CastRange = 3;
} }
} }

View File

@ -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<Character> charaters) public class Team(string name, IEnumerable<Character> charaters)
{ {
public Guid Id { get; set; } = Guid.Empty; public Guid Id { get; set; } = Guid.Empty;
public string Name { get; set; } = name; public string Name { get; set; } = name;
public List<Character> Members { get; } = new(charaters); public List<Character> Members { get; } = [.. charaters];
public int Score { get; set; } = 0; public int Score { get; set; } = 0;
public bool IsWinner { get; set; } = false; public bool IsWinner { get; set; } = false;
public int Count => Members.Count; public int Count => Members.Count;
public List<Character> GetActiveCharacters(IGamingQueue queue) public List<Character> GetActiveCharacters()
{ {
return [.. Members.Where(queue.Queue.Contains)]; return [.. Members.Where(c => c.HP > 0)];
} }
public List<Character> GetTeammates(Character character) public List<Character> GetTeammates(Character character)
@ -21,9 +19,9 @@ namespace Milimoe.FunGame.Core.Entity
return [.. Members.Where(c => c != character)]; return [.. Members.Where(c => c != character)];
} }
public List<Character> GetActiveTeammates(IGamingQueue queue, Character character) public List<Character> 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) public bool IsOnThisTeam(Character character)

View File

@ -25,6 +25,11 @@ namespace Milimoe.FunGame.Core.Interface.Base
/// </summary> /// </summary>
public Dictionary<Guid, Character> Original { get; } public Dictionary<Guid, Character> Original { get; }
/// <summary>
/// 参与本次游戏的所有角色列表
/// </summary>
public List<Character> AllCharacters { get; }
/// <summary> /// <summary>
/// 当前的行动顺序 /// 当前的行动顺序
/// </summary> /// </summary>
@ -151,8 +156,19 @@ namespace Milimoe.FunGame.Core.Interface.Base
/// <param name="caster"></param> /// <param name="caster"></param>
/// <param name="enemys"></param> /// <param name="enemys"></param>
/// <param name="teammates"></param> /// <param name="teammates"></param>
/// <param name="castRange"></param>
/// <param name="desiredTargets"></param>
/// <returns></returns> /// <returns></returns>
public Task<bool> UseItemAsync(Item item, Character caster, List<Character> enemys, List<Character> teammates); public Task<bool> UseItemAsync(Item item, Character caster, List<Character> enemys, List<Character> teammates, List<Grid> castRange, List<Character>? desiredTargets = null);
/// <summary>
/// 角色移动
/// </summary>
/// <param name="character"></param>
/// <param name="target"></param>
/// <param name="startGrid"></param>
/// <returns></returns>
public Task<bool> CharacterMoveAsync(Character character, Grid target, Grid? startGrid);
/// <summary> /// <summary>
/// 选取移动目标 /// 选取移动目标
@ -161,8 +177,9 @@ namespace Milimoe.FunGame.Core.Interface.Base
/// <param name="enemys"></param> /// <param name="enemys"></param>
/// <param name="teammates"></param> /// <param name="teammates"></param>
/// <param name="map"></param> /// <param name="map"></param>
/// <param name="moveRange"></param>
/// <returns></returns> /// <returns></returns>
public Task<Grid> SelectTargetGridAsync(Character character, List<Character> enemys, List<Character> teammates, GameMap map); public Task<Grid> SelectTargetGridAsync(Character character, List<Character> enemys, List<Character> teammates, GameMap map, List<Grid> moveRange);
/// <summary> /// <summary>
/// 选取技能目标 /// 选取技能目标
@ -171,8 +188,9 @@ namespace Milimoe.FunGame.Core.Interface.Base
/// <param name="skill"></param> /// <param name="skill"></param>
/// <param name="enemys"></param> /// <param name="enemys"></param>
/// <param name="teammates"></param> /// <param name="teammates"></param>
/// <param name="castRange"></param>
/// <returns></returns> /// <returns></returns>
public Task<List<Character>> SelectTargetsAsync(Character caster, Skill skill, List<Character> enemys, List<Character> teammates); public Task<List<Character>> SelectTargetsAsync(Character caster, Skill skill, List<Character> enemys, List<Character> teammates, List<Grid> castRange);
/// <summary> /// <summary>
/// 选取普通攻击目标 /// 选取普通攻击目标
@ -181,8 +199,9 @@ namespace Milimoe.FunGame.Core.Interface.Base
/// <param name="attack"></param> /// <param name="attack"></param>
/// <param name="enemys"></param> /// <param name="enemys"></param>
/// <param name="teammates"></param> /// <param name="teammates"></param>
/// <param name="attackRange"></param>
/// <returns></returns> /// <returns></returns>
public Task<List<Character>> SelectTargetsAsync(Character character, NormalAttack attack, List<Character> enemys, List<Character> teammates); public Task<List<Character>> SelectTargetsAsync(Character character, NormalAttack attack, List<Character> enemys, List<Character> teammates, List<Grid> attackRange);
/// <summary> /// <summary>
/// 判断目标对于某个角色是否是队友 /// 判断目标对于某个角色是否是队友

View File

@ -1,4 +1,5 @@
using Milimoe.FunGame.Core.Entity; using Milimoe.FunGame.Core.Entity;
using Milimoe.FunGame.Core.Interface.Base;
namespace Milimoe.FunGame.Core.Interface.Entity namespace Milimoe.FunGame.Core.Interface.Entity
{ {
@ -7,6 +8,11 @@ namespace Milimoe.FunGame.Core.Interface.Entity
/// </summary> /// </summary>
public interface ISkill : IBaseEntity public interface ISkill : IBaseEntity
{ {
/// <summary>
/// 所属的行动顺序表实例
/// </summary>
public IGamingQueue? GamingQueue { get; }
/// <summary> /// <summary>
/// 此技能所属的角色 /// 此技能所属的角色
/// </summary> /// </summary>

View File

@ -344,7 +344,7 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon.Example
return map; return map;
} }
private async Task<Grid> Gq_SelectTargetGrid(GamingQueue queue, Character character, List<Character> enemys, List<Character> teammates, GameMap map) private async Task<Grid> Gq_SelectTargetGrid(GamingQueue queue, Character character, List<Character> enemys, List<Character> teammates, GameMap map, List<Grid> canMoveGrids)
{ {
// 介入选择,假设这里更新界面,让玩家选择目的地 // 介入选择,假设这里更新界面,让玩家选择目的地
await Task.CompletedTask; await Task.CompletedTask;

View File

@ -240,6 +240,50 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
return grids; return grids;
} }
/// <summary>
/// 获取以某个格子为中心,最远距离的格子(曼哈顿距离),只考虑同一平面的格子。
/// </summary>
/// <param name="grid"></param>
/// <param name="range"></param>
/// <param name="includeCharacter"></param>
/// <returns></returns>
public virtual List<Grid> GetOuterGridsByRange(Grid grid, int range, bool includeCharacter = false)
{
List<Grid> 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;
}
/// <summary> /// <summary>
/// 获取以某个格子为中心,一定半径内的格子(圆形范围,欧几里得距离),只考虑同一平面的格子。 /// 获取以某个格子为中心,一定半径内的格子(圆形范围,欧几里得距离),只考虑同一平面的格子。
/// </summary> /// </summary>
@ -254,7 +298,7 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
// 预计算半径的平方 // 预计算半径的平方
int rangeSquared = range * range; int rangeSquared = range * range;
// 遍历以中心格子为中心的方形区域 // 遍历以中心格子为中心的区域
// 范围从 -range 到 +range覆盖所有可能的圆形区域内的格子 // 范围从 -range 到 +range覆盖所有可能的圆形区域内的格子
for (int dx = -range; dx <= range; ++dx) for (int dx = -range; dx <= range; ++dx)
{ {
@ -281,6 +325,47 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
return grids; return grids;
} }
/// <summary>
/// 获取以某个格子为中心,最远距离的格子(圆形范围,欧几里得距离),只考虑同一平面的格子。
/// </summary>
/// <param name="grid"></param>
/// <param name="range"></param>
/// <param name="includeCharacter"></param>
/// <returns></returns>
public virtual List<Grid> GetOuterGridsByCircleRange(Grid grid, int range, bool includeCharacter = false)
{
List<Grid> 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;
}
/// <summary> /// <summary>
/// 设置角色移动 /// 设置角色移动
/// </summary> /// </summary>
@ -296,9 +381,15 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
} }
Grid? realGrid = GetCharacterCurrentGrid(character); Grid? realGrid = GetCharacterCurrentGrid(character);
Grid startGrid = current;
if (current.Id == target.Id) if (realGrid != null && current.Id != realGrid.Id)
{ {
startGrid = realGrid;
}
if (startGrid.Id == target.Id)
{
SetCharacterCurrentGrid(character, startGrid);
return 0; return 0;
} }
@ -308,8 +399,8 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
HashSet<long> visited = []; HashSet<long> visited = [];
// 将起始格子加入队列步数为0并标记为已访问 // 将起始格子加入队列步数为0并标记为已访问
queue.Enqueue((current, 0)); queue.Enqueue((startGrid, 0));
visited.Add(current.Id); visited.Add(startGrid.Id);
while (queue.Count > 0) while (queue.Count > 0)
{ {
@ -319,9 +410,7 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
if (currentGrid.Id == target.Id) if (currentGrid.Id == target.Id)
{ {
realGrid?.Characters.Remove(character); realGrid?.Characters.Remove(character);
current.Characters.Remove(character); SetCharacterCurrentGrid(character, target);
target.Characters.Add(character);
Characters[character] = target;
return currentSteps; return currentSteps;
} }
@ -369,10 +458,15 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
} }
Grid? realGrid = GetCharacterCurrentGrid(character); Grid? realGrid = GetCharacterCurrentGrid(character);
Grid startGrid = current;
if (current.Id == target.Id) if (realGrid != null && current.Id != realGrid.Id)
{ {
SetCharacterCurrentGrid(character, current); startGrid = realGrid;
}
if (startGrid.Id == target.Id)
{
SetCharacterCurrentGrid(character, startGrid);
return 0; return 0;
} }
@ -383,11 +477,11 @@ namespace Milimoe.FunGame.Core.Library.Common.Addon
HashSet<long> visited = []; HashSet<long> visited = [];
// 初始化 BFS 队列将起始格子加入步数为0 // 初始化 BFS 队列将起始格子加入步数为0
queue.Enqueue((current, 0)); queue.Enqueue((startGrid, 0));
visited.Add(current.Id); visited.Add(startGrid.Id);
Grid? bestReachableGrid = current; Grid? bestReachableGrid = current;
int minDistanceToTarget = CalculateManhattanDistance(current, target); int minDistanceToTarget = CalculateManhattanDistance(startGrid, target);
int stepsToBestReachable = 0; int stepsToBestReachable = 0;
// 定义平面移动的四个方向 // 定义平面移动的四个方向

View File

@ -83,6 +83,9 @@ namespace Milimoe.FunGame.Core.Library.Common.JsonConverter
case nameof(Character.ExHPPercentage): case nameof(Character.ExHPPercentage):
result.ExHPPercentage = reader.GetDouble(); result.ExHPPercentage = reader.GetDouble();
break; break;
case nameof(Character.HasMP):
result.HasMP = reader.GetBoolean();
break;
case nameof(Character.InitialMP): case nameof(Character.InitialMP):
result.InitialMP = reader.GetDouble(); result.InitialMP = reader.GetDouble();
break; break;
@ -197,11 +200,11 @@ namespace Milimoe.FunGame.Core.Library.Common.JsonConverter
case nameof(Character.ExCDR): case nameof(Character.ExCDR):
result.ExCDR = reader.GetDouble(); result.ExCDR = reader.GetDouble();
break; break;
case nameof(Character.ATR): case nameof(Character.ExATR):
result.ATR = reader.GetInt32(); result.ExATR = reader.GetInt32();
break; break;
case nameof(Character.MOV): case nameof(Character.ExMOV):
result.MOV = reader.GetInt32(); result.ExMOV = reader.GetInt32();
break; break;
case nameof(Character.ExCritRate): case nameof(Character.ExCritRate):
result.ExCritRate = reader.GetDouble(); 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.InitialHP), value.InitialHP);
writer.WriteNumber(nameof(Character.ExHP2), value.ExHP2); writer.WriteNumber(nameof(Character.ExHP2), value.ExHP2);
writer.WriteNumber(nameof(Character.ExHPPercentage), value.ExHPPercentage); 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.InitialMP), value.InitialMP);
writer.WriteNumber(nameof(Character.ExMP2), value.ExMP2); writer.WriteNumber(nameof(Character.ExMP2), value.ExMP2);
writer.WriteNumber(nameof(Character.ExMPPercentage), value.ExMPPercentage); 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.ExActionCoefficient), value.ExActionCoefficient);
writer.WriteNumber(nameof(Character.ExAccelerationCoefficient), value.ExAccelerationCoefficient); writer.WriteNumber(nameof(Character.ExAccelerationCoefficient), value.ExAccelerationCoefficient);
writer.WriteNumber(nameof(Character.ExCDR), value.ExCDR); writer.WriteNumber(nameof(Character.ExCDR), value.ExCDR);
writer.WriteNumber(nameof(Character.ATR), value.ATR); writer.WriteNumber(nameof(Character.ExATR), value.ExATR);
writer.WriteNumber(nameof(Character.MOV), value.MOV); writer.WriteNumber(nameof(Character.ExMOV), value.ExMOV);
writer.WriteNumber(nameof(Character.ExCritRate), value.ExCritRate); writer.WriteNumber(nameof(Character.ExCritRate), value.ExCritRate);
writer.WriteNumber(nameof(Character.ExCritDMG), value.ExCritDMG); writer.WriteNumber(nameof(Character.ExCritDMG), value.ExCritDMG);
writer.WriteNumber(nameof(Character.ExEvadeRate), value.ExEvadeRate); writer.WriteNumber(nameof(Character.ExEvadeRate), value.ExEvadeRate);

View File

@ -1063,4 +1063,14 @@ namespace Milimoe.FunGame.Core.Library.Constant
Magical, Magical,
True True
} }
public enum SkillRangeType
{
Diamond,
Circle,
Square,
Line,
LinePass,
Sector
}
} }

18
Model/AIDecision.cs Normal file
View File

@ -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<Character> Targets { get; set; } = [];
public double Score { get; set; } = 0;
public bool IsPureMove { get; set; } = false;
}
}

View File

@ -460,6 +460,86 @@ namespace Milimoe.FunGame.Core.Model
/// </summary> /// </summary>
public double HiddenWeaponHardness { get; set; } = 7; public double HiddenWeaponHardness { get; set; } = 7;
/// <summary>
/// 单手剑的攻击距离
/// </summary>
public int OneHandedSwordAttackRange { get; set; } = 1;
/// <summary>
/// 双手剑的攻击距离
/// </summary>
public int TwoHandedSwordAttackRange { get; set; } = 2;
/// <summary>
/// 弓的攻击距离
/// </summary>
public int BowAttackRange { get; set; } = 4;
/// <summary>
/// 手枪的攻击距离
/// </summary>
public int PistolAttackRange { get; set; } = 3;
/// <summary>
/// 步枪的攻击距离
/// </summary>
public int RifleAttackRange { get; set; } = 5;
/// <summary>
/// 双持短刀的攻击距离
/// </summary>
public int DualDaggersAttackRange { get; set; } = 1;
/// <summary>
/// 法器的攻击距离
/// </summary>
public int TalismanAttackRange { get; set; } = 5;
/// <summary>
/// 法杖的攻击距离
/// </summary>
public int StaffAttackRange { get; set; } = 3;
/// <summary>
/// 长柄武器的攻击距离
/// </summary>
public int PolearmAttackRange { get; set; } = 2;
/// <summary>
/// 拳套的攻击距离
/// </summary>
public int GauntletAttackRange { get; set; } = 1;
/// <summary>
/// 暗器的攻击距离
/// </summary>
public int HiddenWeaponAttackRange { get; set; } = 4;
/// <summary>
/// 核心角色的移动距离
/// </summary>
public int RoleMOV_Core { get; set; } = 3;
/// <summary>
/// 先锋角色的移动距离
/// </summary>
public int RoleMOV_Vanguard { get; set; } = 6;
/// <summary>
/// 近卫角色的移动距离
/// </summary>
public int RoleMOV_Guardian { get; set; } = 5;
/// <summary>
/// 支援角色的移动距离
/// </summary>
public int RoleMOV_Support { get; set; } = 4;
/// <summary>
/// 治疗角色的移动距离
/// </summary>
public int RoleMOV_Medic { get; set; } = 3;
/// <summary> /// <summary>
/// 应用此游戏平衡常数给实体 /// 应用此游戏平衡常数给实体
/// </summary> /// </summary>

View File

@ -1,4 +1,5 @@
using Milimoe.FunGame.Core.Api.Utility; using Milimoe.FunGame.Core.Api.Utility;
using Milimoe.FunGame.Core.Controller;
using Milimoe.FunGame.Core.Entity; using Milimoe.FunGame.Core.Entity;
using Milimoe.FunGame.Core.Interface.Base; using Milimoe.FunGame.Core.Interface.Base;
using Milimoe.FunGame.Core.Interface.Entity; 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<Character> allTeammates = GetTeammates(character); List<Character> allTeammates = GetTeammates(character);
List<Character> teammates = [.. allTeammates.Where(_queue.Contains)]; List<Character> selecableTeammates = [.. allTeammates.Where(_queue.Contains)];
// 敌人列表 // 敌人列表
List<Character> allEnemys = [.. _allCharacters.Where(c => c != character && !teammates.Contains(c))]; List<Character> allEnemys = [.. _allCharacters.Where(c => c != character && !allTeammates.Contains(c))];
List<Character> enemys = [.. allEnemys.Where(c => _queue.Contains(c) && !c.IsUnselectable)]; List<Character> selectableEnemys = [.. allEnemys.Where(c => _queue.Contains(c) && !c.IsUnselectable)];
// 技能列表 // 技能列表
List<Skill> skills = [.. character.Skills.Where(s => s.Level > 0 && s.SkillType != SkillType.Passive && s.Enable && !s.IsInEffect && s.CurrentCD == 0 && List<Skill> 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 接管回合操作 // 回合开始事件,允许事件返回 false 接管回合操作
// 如果事件全程接管回合操作,需要注意触发特效 // 如果事件全程接管回合操作,需要注意触发特效
if (!await OnTurnStartAsync(character, enemys, teammates, skills, items)) if (!await OnTurnStartAsync(character, selectableEnemys, selecableTeammates, skills, items))
{ {
_isInRound = false; _isInRound = false;
return _isGameEnd; return _isGameEnd;
@ -836,13 +839,13 @@ namespace Milimoe.FunGame.Core.Model
foreach (Skill skillTurnStart in skills) foreach (Skill skillTurnStart in skills)
{ {
skillTurnStart.OnTurnStart(character, enemys, teammates, skills, items); skillTurnStart.OnTurnStart(character, selectableEnemys, selecableTeammates, skills, items);
} }
List<Effect> effects = [.. character.Effects.Where(e => e.IsInEffect)]; List<Effect> effects = [.. character.Effects.Where(e => e.IsInEffect)];
foreach (Effect effect in effects) 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; bool moved = false;
Grid? currentGrid = null; // AI 决策控制器,适用于启用战棋地图的情况
AIController? ai = null;
// 角色的起始地点,确保角色该回合移动的范围不超过 MOV
Grid? startGrid = null;
List<Grid> canMoveGrids = [];
// 并且要筛选最远可选取角色
List<Grid> canAttackGridsByStartGrid = [];
List<Grid> canCastGridsByStartGrid = [];
if (_map != null) if (_map != null)
{ {
currentGrid = _map.GetCharacterCurrentGrid(character); startGrid = _map.GetCharacterCurrentGrid(character);
}
// 行动开始前,可以修改可选取的角色列表 if (startGrid != null)
Dictionary<Character, int> continuousKillingTemp = new(_continuousKilling); {
Dictionary<Character, int> earnedMoneyTemp = new(_earnedMoney); canMoveGrids = _map.GetGridsByRange(startGrid, character.MOV, false);
effects = [.. character.Effects.Where(e => e.IsInEffect)]; canAttackGridsByStartGrid = _map.GetGridsByRange(startGrid, character.ATR, true);
foreach (Effect effect in effects) Skill[] canCastSkills = [.. skills, .. items.Select(i => i.Skills.Active!)];
{ foreach (Skill skill in canCastSkills)
effect.AlterSelectListBeforeAction(character, enemys, teammates, skills, continuousKillingTemp, earnedMoneyTemp); {
} canCastGridsByStartGrid.AddRange(_map.GetGridsByRange(startGrid, skill.CastRange, true));
}
}
// 这里筛掉重复角色 allEnemys = [.. allEnemys.Where(canAttackGridsByStartGrid.Union(canCastGridsByStartGrid).SelectMany(g => g.Characters).Contains)];
enemys = [.. enemys.Distinct()]; allTeammates = [.. allTeammates.Where(canAttackGridsByStartGrid.Union(canCastGridsByStartGrid).SelectMany(g => g.Characters).Contains)];
teammates = [.. teammates.Distinct()]; }
// 作出了什么行动 // 作出了什么行动
CharacterActionType type = CharacterActionType.None; CharacterActionType type = CharacterActionType.None;
@ -880,10 +892,62 @@ namespace Milimoe.FunGame.Core.Model
bool isAI = CharactersInAI.Contains(character); bool isAI = CharactersInAI.Contains(character);
while (!decided && (!isAI || cancelTimes > 0)) while (!decided && (!isAI || cancelTimes > 0))
{ {
// 根据当前位置,更新可选取角色列表
Grid? realGrid = null;
List<Grid> canAttackGrids = [];
List<Grid> canCastGrids = [];
List<Grid> willMoveGridWithSkill = [];
List<Character> enemys = [];
List<Character> 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<Character, int> continuousKillingTemp = new(_continuousKilling);
Dictionary<Character, int> 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; if (moved) moved = false;
else cancelTimes--; else cancelTimes--;
type = CharacterActionType.None; type = CharacterActionType.None;
// 是否能使用物品和释放技能 // 是否能使用物品和释放技能
bool canUseItem = items.Count > 0; bool canUseItem = items.Count > 0;
bool canCastSkill = skills.Count > 0; bool canCastSkill = skills.Count > 0;
@ -996,8 +1060,17 @@ namespace Milimoe.FunGame.Core.Model
} }
} }
// 模组可以通过此事件来决定角色的行动 // 启用战棋地图时的专属 AI 决策方法
type = await OnDecideActionAsync(character, enemys, teammates, skills, items); 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) 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 || if (!forceAction && (character.CharacterState == CharacterState.NotActionable ||
character.CharacterState == CharacterState.ActionRestricted || character.CharacterState == CharacterState.ActionRestricted ||
@ -1033,7 +1133,22 @@ namespace Milimoe.FunGame.Core.Model
else else
{ {
// 使用普通攻击逻辑 // 使用普通攻击逻辑
List<Character> targets = await SelectTargetsAsync(character, character.NormalAttack, enemys, teammates); List<Character> targets;
if (aiDecision != null)
{
targets = aiDecision.Targets;
}
else
{
List<Grid> 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) if (targets.Count > 0)
{ {
LastRound.Targets = [.. targets]; LastRound.Targets = [.. targets];
@ -1063,7 +1178,15 @@ namespace Milimoe.FunGame.Core.Model
else 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) if (skill is null && CharactersInAI.Contains(character) && skills.Count > 0)
{ {
skill = skills[Random.Shared.Next(skills.Count)]; skill = skills[Random.Shared.Next(skills.Count)];
@ -1073,7 +1196,22 @@ namespace Milimoe.FunGame.Core.Model
// 吟唱前需要先选取目标 // 吟唱前需要先选取目标
if (skill.SkillType == SkillType.Magic) if (skill.SkillType == SkillType.Magic)
{ {
List<Character> targets = await SelectTargetsAsync(character, skill, enemys, teammates); List<Character> targets;
if (aiDecision != null)
{
targets = aiDecision.Targets;
}
else
{
List<Grid> 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) if (targets.Count > 0)
{ {
// 免疫检定 // 免疫检定
@ -1100,7 +1238,22 @@ namespace Milimoe.FunGame.Core.Model
// 只有魔法需要吟唱,战技和爆发技直接释放 // 只有魔法需要吟唱,战技和爆发技直接释放
if (CheckCanCast(character, skill, out double cost)) if (CheckCanCast(character, skill, out double cost))
{ {
List<Character> targets = await SelectTargetsAsync(character, skill, enemys, teammates); List<Character> targets;
if (aiDecision != null)
{
targets = aiDecision.Targets;
}
else
{
List<Grid> 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) if (targets.Count > 0)
{ {
// 免疫检定 // 免疫检定
@ -1211,7 +1364,8 @@ namespace Milimoe.FunGame.Core.Model
if (CheckCanCast(character, skill, out double cost)) if (CheckCanCast(character, skill, out double cost))
{ {
// 预释放的爆发技不可取消 // 预释放的爆发技不可取消
List<Character> targets = await SelectTargetsAsync(character, skill, enemys, teammates); List<Grid> castRange = _map != null && realGrid != null ? _map.GetGridsByRange(realGrid, skill.CastRange, true) : [];
List<Character> targets = await SelectTargetsAsync(character, skill, enemys, teammates, castRange);
// 免疫检定 // 免疫检定
await CheckSkilledImmuneAsync(character, targets, skill); await CheckSkilledImmuneAsync(character, targets, skill);
LastRound.Targets = [.. targets]; LastRound.Targets = [.. targets];
@ -1246,7 +1400,15 @@ namespace Milimoe.FunGame.Core.Model
else if (type == CharacterActionType.UseItem) 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) if (item is null && CharactersInAI.Contains(character) && items.Count > 0)
{ {
// AI 控制下随机选取一个物品 // AI 控制下随机选取一个物品
@ -1255,7 +1417,14 @@ namespace Milimoe.FunGame.Core.Model
if (item != null && item.Skills.Active != null) if (item != null && item.Skills.Active != null)
{ {
Skill skill = item.Skills.Active; Skill skill = item.Skills.Active;
if (await UseItemAsync(item, character, enemys, teammates)) List<Grid> 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; decided = true;
LastRound.Item = item; LastRound.Item = item;
@ -1282,20 +1451,6 @@ namespace Milimoe.FunGame.Core.Model
WriteLine($"[ {character} ] 结束了回合!"); WriteLine($"[ {character} ] 结束了回合!");
await OnCharacterDoNothingAsync(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 else
{ {
if (baseTime == 0) baseTime += 8; if (baseTime == 0) baseTime += 8;
@ -2213,8 +2368,10 @@ namespace Milimoe.FunGame.Core.Model
/// <param name="character"></param> /// <param name="character"></param>
/// <param name="enemys"></param> /// <param name="enemys"></param>
/// <param name="teammates"></param> /// <param name="teammates"></param>
/// <param name="castRange"></param>
/// <param name="desiredTargets"></param>
/// <returns></returns> /// <returns></returns>
public async Task<bool> UseItemAsync(Item item, Character character, List<Character> enemys, List<Character> teammates) public async Task<bool> UseItemAsync(Item item, Character character, List<Character> enemys, List<Character> teammates, List<Grid> castRange, List<Character>? desiredTargets = null)
{ {
if (CheckCanCast(character, item, out double costMP, out double costEP)) if (CheckCanCast(character, item, out double costMP, out double costEP))
{ {
@ -2222,7 +2379,15 @@ namespace Milimoe.FunGame.Core.Model
if (skill != null) if (skill != null)
{ {
skill.GamingQueue = this; skill.GamingQueue = this;
List<Character> targets = await SelectTargetsAsync(character, skill, enemys, teammates); List<Character> targets;
if (desiredTargets != null)
{
targets = desiredTargets;
}
else
{
targets = await SelectTargetsAsync(character, skill, enemys, teammates, castRange);
}
if (targets.Count > 0) if (targets.Count > 0)
{ {
// 免疫检定 // 免疫检定
@ -2277,6 +2442,28 @@ namespace Milimoe.FunGame.Core.Model
return false; return false;
} }
/// <summary>
/// 角色移动实际逻辑
/// </summary>
/// <param name="character"></param>
/// <param name="target"></param>
/// <param name="startGrid"></param>
/// <returns></returns>
public async Task<bool> 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;
}
/// <summary> /// <summary>
/// 通过概率计算角色要干嘛 /// 通过概率计算角色要干嘛
/// </summary> /// </summary>
@ -2329,25 +2516,25 @@ namespace Milimoe.FunGame.Core.Model
/// <param name="enemys"></param> /// <param name="enemys"></param>
/// <param name="teammates"></param> /// <param name="teammates"></param>
/// <param name="map"></param> /// <param name="map"></param>
/// <param name="moveRange"></param>
/// <returns></returns> /// <returns></returns>
public async Task<Grid> SelectTargetGridAsync(Character character, List<Character> enemys, List<Character> teammates, GameMap map) public async Task<Grid> SelectTargetGridAsync(Character character, List<Character> enemys, List<Character> teammates, GameMap map, List<Grid> moveRange)
{ {
List<Effect> effects = [.. character.Effects.Where(e => e.IsInEffect)]; List<Effect> effects = [.. character.Effects.Where(e => e.IsInEffect)];
foreach (Effect effect in effects) 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) if (target.Id != -1)
{ {
return target; return target;
} }
else if (target.Id == -2 && map.Characters.TryGetValue(character, out Grid? current) && current != null) else if (target.Id == -2 && map.Characters.TryGetValue(character, out Grid? current) && current != null)
{ {
List<Grid> grids = map.GetGridsByRange(current, character.MOV); if (moveRange.Count > 0)
if (grids.Count > 0)
{ {
return grids[Random.Shared.Next(grids.Count)]; return moveRange[Random.Shared.Next(moveRange.Count)];
} }
} }
return Grid.Empty; return Grid.Empty;
@ -2360,15 +2547,16 @@ namespace Milimoe.FunGame.Core.Model
/// <param name="skill"></param> /// <param name="skill"></param>
/// <param name="enemys"></param> /// <param name="enemys"></param>
/// <param name="teammates"></param> /// <param name="teammates"></param>
/// <param name="castRange"></param>
/// <returns></returns> /// <returns></returns>
public async Task<List<Character>> SelectTargetsAsync(Character caster, Skill skill, List<Character> enemys, List<Character> teammates) public async Task<List<Character>> SelectTargetsAsync(Character caster, Skill skill, List<Character> enemys, List<Character> teammates, List<Grid> castRange)
{ {
List<Effect> effects = [.. caster.Effects.Where(e => e.IsInEffect)]; List<Effect> effects = [.. caster.Effects.Where(e => e.IsInEffect)];
foreach (Effect effect in effects) foreach (Effect effect in effects)
{ {
effect.AlterSelectListBeforeSelection(caster, skill, enemys, teammates); effect.AlterSelectListBeforeSelection(caster, skill, enemys, teammates);
} }
List<Character> targets = await OnSelectSkillTargetsAsync(caster, skill, enemys, teammates); List<Character> targets = await OnSelectSkillTargetsAsync(caster, skill, enemys, teammates, castRange);
if (targets.Count == 0 && CharactersInAI.Contains(caster)) if (targets.Count == 0 && CharactersInAI.Contains(caster))
{ {
targets = skill.SelectTargets(caster, enemys, teammates); targets = skill.SelectTargets(caster, enemys, teammates);
@ -2383,15 +2571,16 @@ namespace Milimoe.FunGame.Core.Model
/// <param name="attack"></param> /// <param name="attack"></param>
/// <param name="enemys"></param> /// <param name="enemys"></param>
/// <param name="teammates"></param> /// <param name="teammates"></param>
/// <param name="attackRange"></param>
/// <returns></returns> /// <returns></returns>
public async Task<List<Character>> SelectTargetsAsync(Character character, NormalAttack attack, List<Character> enemys, List<Character> teammates) public async Task<List<Character>> SelectTargetsAsync(Character character, NormalAttack attack, List<Character> enemys, List<Character> teammates, List<Grid> attackRange)
{ {
List<Effect> effects = [.. character.Effects.Where(e => e.IsInEffect)]; List<Effect> effects = [.. character.Effects.Where(e => e.IsInEffect)];
foreach (Effect effect in effects) foreach (Effect effect in effects)
{ {
effect.AlterSelectListBeforeSelection(character, attack, enemys, teammates); effect.AlterSelectListBeforeSelection(character, attack, enemys, teammates);
} }
List<Character> targets = await OnSelectNormalAttackTargetsAsync(character, attack, enemys, teammates); List<Character> targets = await OnSelectNormalAttackTargetsAsync(character, attack, enemys, teammates, attackRange);
if (targets.Count == 0 && CharactersInAI.Contains(character)) if (targets.Count == 0 && CharactersInAI.Contains(character))
{ {
targets = character.NormalAttack.SelectTargets(character, enemys, teammates); 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<Item?>(null)); return await (SelectItem?.Invoke(this, character, items) ?? Task.FromResult<Item?>(null));
} }
public delegate Task<Grid> SelectTargetGridEventHandler(GamingQueue queue, Character character, List<Character> enemys, List<Character> teammates, GameMap map); public delegate Task<Grid> SelectTargetGridEventHandler(GamingQueue queue, Character character, List<Character> enemys, List<Character> teammates, GameMap map, List<Grid> moveRange);
/// <summary> /// <summary>
/// 选取移动目标事件 /// 选取移动目标事件
/// </summary> /// </summary>
@ -3371,13 +3560,14 @@ namespace Milimoe.FunGame.Core.Model
/// <param name="enemys"></param> /// <param name="enemys"></param>
/// <param name="teammates"></param> /// <param name="teammates"></param>
/// <param name="map"></param> /// <param name="map"></param>
/// <param name="moveRange"></param>
/// <returns></returns> /// <returns></returns>
protected async Task<Grid> OnSelectTargetGridAsync(Character character, List<Character> enemys, List<Character> teammates, GameMap map) protected async Task<Grid> OnSelectTargetGridAsync(Character character, List<Character> enemys, List<Character> teammates, GameMap map, List<Grid> 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<List<Character>> SelectSkillTargetsEventHandler(GamingQueue queue, Character caster, Skill skill, List<Character> enemys, List<Character> teammates); public delegate Task<List<Character>> SelectSkillTargetsEventHandler(GamingQueue queue, Character caster, Skill skill, List<Character> enemys, List<Character> teammates, List<Grid> castRange);
/// <summary> /// <summary>
/// 选取技能目标事件 /// 选取技能目标事件
/// </summary> /// </summary>
@ -3389,13 +3579,14 @@ namespace Milimoe.FunGame.Core.Model
/// <param name="skill"></param> /// <param name="skill"></param>
/// <param name="enemys"></param> /// <param name="enemys"></param>
/// <param name="teammates"></param> /// <param name="teammates"></param>
/// <param name="castRange"></param>
/// <returns></returns> /// <returns></returns>
protected async Task<List<Character>> OnSelectSkillTargetsAsync(Character caster, Skill skill, List<Character> enemys, List<Character> teammates) protected async Task<List<Character>> OnSelectSkillTargetsAsync(Character caster, Skill skill, List<Character> enemys, List<Character> teammates, List<Grid> castRange)
{ {
return await (SelectSkillTargets?.Invoke(this, caster, skill, enemys, teammates) ?? Task.FromResult(new List<Character>())); return await (SelectSkillTargets?.Invoke(this, caster, skill, enemys, teammates, castRange) ?? Task.FromResult(new List<Character>()));
} }
public delegate Task<List<Character>> SelectNormalAttackTargetsEventHandler(GamingQueue queue, Character character, NormalAttack attack, List<Character> enemys, List<Character> teammates); public delegate Task<List<Character>> SelectNormalAttackTargetsEventHandler(GamingQueue queue, Character character, NormalAttack attack, List<Character> enemys, List<Character> teammates, List<Grid> attackRange);
/// <summary> /// <summary>
/// 选取普通攻击目标事件 /// 选取普通攻击目标事件
/// </summary> /// </summary>
@ -3407,10 +3598,11 @@ namespace Milimoe.FunGame.Core.Model
/// <param name="attack"></param> /// <param name="attack"></param>
/// <param name="enemys"></param> /// <param name="enemys"></param>
/// <param name="teammates"></param> /// <param name="teammates"></param>
/// <param name="attackRange"></param>
/// <returns></returns> /// <returns></returns>
protected async Task<List<Character>> OnSelectNormalAttackTargetsAsync(Character character, NormalAttack attack, List<Character> enemys, List<Character> teammates) protected async Task<List<Character>> OnSelectNormalAttackTargetsAsync(Character character, NormalAttack attack, List<Character> enemys, List<Character> teammates, List<Grid> attackRange)
{ {
return await (SelectNormalAttackTargets?.Invoke(this, character, attack, enemys, teammates) ?? Task.FromResult(new List<Character>())); return await (SelectNormalAttackTargets?.Invoke(this, character, attack, enemys, teammates, attackRange) ?? Task.FromResult(new List<Character>()));
} }
public delegate Task InterruptCastingEventHandler(GamingQueue queue, Character cast, Skill? skill, Character interrupter); public delegate Task InterruptCastingEventHandler(GamingQueue queue, Character cast, Skill? skill, Character interrupter);

View File

@ -147,7 +147,7 @@ namespace Milimoe.FunGame.Core.Model
{ {
string[] teamActive = [.. Teams.OrderByDescending(kv => kv.Value.Score).Select(kv => 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) if (kv.Value == killTeam)
{ {
activeCount += 1; activeCount += 1;
@ -159,7 +159,7 @@ namespace Milimoe.FunGame.Core.Model
if (deathTeam != null) if (deathTeam != null)
{ {
List<Character> remain = deathTeam.GetActiveCharacters(this); List<Character> remain = deathTeam.GetActiveCharacters();
int remainCount = remain.Count; int remainCount = remain.Count;
if (remainCount == 0) if (remainCount == 0)
{ {
@ -175,7 +175,7 @@ namespace Milimoe.FunGame.Core.Model
if (killTeam != null) if (killTeam != null)
{ {
List<Character> actives = killTeam.GetActiveCharacters(this); List<Character> actives = killTeam.GetActiveCharacters();
int remainCount = actives.Count; int remainCount = actives.Count;
if (remainCount > 0 && MaxRespawnTimes == 0) if (remainCount > 0 && MaxRespawnTimes == 0)
{ {