From ea22adf9cd2a929e10f250855716bb61dfd0c1b0 Mon Sep 17 00:00:00 2001 From: milimoe <110188673+milimoe@users.noreply.github.com> Date: Thu, 1 Jan 2026 04:48:10 +0800 Subject: [PATCH] =?UTF-8?q?=E5=86=B3=E7=AD=96=E7=82=B9=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=EF=BC=9A=E5=9B=9E=E5=90=88=E5=86=85=E5=A4=9A=E6=AC=A1=E8=A1=8C?= =?UTF-8?q?=E5=8A=A8=20(#144)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 添加决策点功能 * 修复回合日志记录错误 * 吟唱和结算未正确显示 * 事件传递决策点参数;修复第二次行动相关列表不更新的问题 * 决策点可定制;添加事件;事件和钩子优化;修复BUG --- Controller/AIController.cs | 50 +- Entity/Item/Item.cs | 5 +- Entity/Skill/Effect.cs | 33 +- Entity/Skill/NormalAttack.cs | 16 +- Interface/Base/IGamingQueue.cs | 13 +- .../JsonConverter/RoundRecordConverter.cs | 65 +- Model/DecisionPoints.cs | 169 ++ Model/EquilibriumConstant.cs | 80 + Model/GamingQueue.cs | 1456 ++++++++++------- Model/MixGamingQueue.cs | 19 + Model/RoundRecord.cs | 94 +- Model/TeamGamingQueue.cs | 32 +- 12 files changed, 1369 insertions(+), 663 deletions(-) create mode 100644 Model/DecisionPoints.cs diff --git a/Controller/AIController.cs b/Controller/AIController.cs index ea453f2..7588ba7 100644 --- a/Controller/AIController.cs +++ b/Controller/AIController.cs @@ -12,19 +12,20 @@ namespace Milimoe.FunGame.Core.Controller private readonly GameMap _map = map; /// - /// AI的核心决策方法,根据当前游戏状态为角色选择最佳行动。 + /// AI的核心决策方法,根据当前游戏状态为角色选择最佳行动 /// - /// 当前行动的AI角色。 - /// 角色的起始格子。 - /// 从起始格子可达的所有移动格子(包括起始格子本身)。 - /// 角色所有可用的技能(已过滤CD和EP/MP)。 - /// 角色所有可用的物品(已过滤CD和EP/MP)。 - /// 场上所有敌人。 - /// 场上所有队友。 - /// 场上能够选取的敌人。 - /// 场上能够选取的队友。 - /// 包含最佳行动的AIDecision对象。 - public async Task DecideAIActionAsync(Character character, Grid startGrid, List allPossibleMoveGrids, + /// 当前行动的AI角色 + /// 角色的决策点 + /// 角色的起始格子 + /// 从起始格子可达的所有移动格子(包括起始格子本身) + /// 角色所有可用的技能(已过滤CD和EP/MP) + /// 角色所有可用的物品(已过滤CD和EP/MP) + /// 场上所有敌人 + /// 场上所有队友 + /// 场上能够选取的敌人 + /// 场上能够选取的队友 + /// 包含最佳行动的AIDecision对象 + public async Task DecideAIActionAsync(Character character, DecisionPoints dp, Grid startGrid, List allPossibleMoveGrids, List availableSkills, List availableItems, List allEnemysInGame, List allTeammatesInGame, List selectableEnemys, List selectableTeammates) { @@ -44,7 +45,7 @@ namespace Milimoe.FunGame.Core.Controller int moveDistance = GameMap.CalculateManhattanDistance(startGrid, potentialMoveGrid); double movePenalty = moveDistance * 0.5; // 每移动一步扣0.5分 - if (CanCharacterNormalAttack(character)) + if (CanCharacterNormalAttack(character, dp)) { // 计算普通攻击的可达格子 List normalAttackReachableGrids = _map.GetGridsByRange(potentialMoveGrid, character.ATR, true); @@ -80,7 +81,7 @@ namespace Milimoe.FunGame.Core.Controller foreach (Skill skill in availableSkills) { - if (CanCharacterUseSkill(character) && _queue.CheckCanCast(character, skill, out double cost)) + if (CanCharacterUseSkill(character, skill, dp) && _queue.CheckCanCast(character, skill, out double cost)) { // 计算当前技能的可达格子 List skillReachableGrids = _map.GetGridsByRange(potentialMoveGrid, skill.CastRange, true); @@ -118,7 +119,7 @@ namespace Milimoe.FunGame.Core.Controller foreach (Item item in availableItems) { - if (item.Skills.Active != null && CanCharacterUseItem(character, item) && _queue.CheckCanCast(character, item.Skills.Active, out double cost)) + if (item.Skills.Active != null && CanCharacterUseItem(character, item, dp) && _queue.CheckCanCast(character, item.Skills.Active, out double cost)) { Skill itemSkill = item.Skills.Active; @@ -158,7 +159,7 @@ namespace Milimoe.FunGame.Core.Controller } // 如果从该格子没有更好的行动,但移动本身有价值 - // 只有当当前最佳决策是“结束回合”或分数很低时,才考虑纯粹的移动。 + // 只有当当前最佳决策是“结束回合”或分数很低时,才考虑纯粹的移动 if (potentialMoveGrid != startGrid && bestDecision.Score < 0) // 如果当前最佳决策是负分(即什么都不做) { double pureMoveScore = -movePenalty; // 移动本身有代价 @@ -221,27 +222,32 @@ namespace Milimoe.FunGame.Core.Controller // --- AI 决策辅助方法 --- // 检查角色是否能进行普通攻击(基于状态) - private static bool CanCharacterNormalAttack(Character character) + private static bool CanCharacterNormalAttack(Character character, DecisionPoints dp) { - return character.CharacterState != CharacterState.NotActionable && + return dp.CheckActionTypeQuota(CharacterActionType.NormalAttack) && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostNormalAttack && + character.CharacterState != CharacterState.NotActionable && character.CharacterState != CharacterState.ActionRestricted && character.CharacterState != CharacterState.BattleRestricted && character.CharacterState != CharacterState.AttackRestricted; } // 检查角色是否能使用某个技能(基于状态) - private static bool CanCharacterUseSkill(Character character) + private static bool CanCharacterUseSkill(Character character, Skill skill, DecisionPoints dp) { - return character.CharacterState != CharacterState.NotActionable && + return ((skill.SkillType == SkillType.Magic && dp.CheckActionTypeQuota(CharacterActionType.PreCastSkill) && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostMagic) || + (skill.SkillType == SkillType.Skill && dp.CheckActionTypeQuota(CharacterActionType.CastSkill) && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostSkill) || + (skill.SkillType == SkillType.SuperSkill && dp.CheckActionTypeQuota(CharacterActionType.CastSuperSkill)) && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostSuperSkill) && + character.CharacterState != CharacterState.NotActionable && character.CharacterState != CharacterState.ActionRestricted && character.CharacterState != CharacterState.BattleRestricted && character.CharacterState != CharacterState.SkillRestricted; } // 检查角色是否能使用某个物品(基于状态) - private static bool CanCharacterUseItem(Character character, Item item) + private static bool CanCharacterUseItem(Character character, Item item, DecisionPoints dp) { - return character.CharacterState != CharacterState.NotActionable && + return dp.CheckActionTypeQuota(CharacterActionType.UseItem) && dp.CurrentDecisionPoints > dp.GameplayEquilibriumConstant.DecisionPointsCostItem && + character.CharacterState != CharacterState.NotActionable && (character.CharacterState != CharacterState.ActionRestricted || item.ItemType == ItemType.Consumable) && // 行动受限只能用消耗品 character.CharacterState != CharacterState.BattleRestricted; } diff --git a/Entity/Item/Item.cs b/Entity/Item/Item.cs index 81fbe3c..932266f 100644 --- a/Entity/Item/Item.cs +++ b/Entity/Item/Item.cs @@ -4,6 +4,7 @@ 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; +using Milimoe.FunGame.Core.Model; namespace Milimoe.FunGame.Core.Entity { @@ -310,7 +311,7 @@ namespace Milimoe.FunGame.Core.Entity /// 局内使用物品触发 /// /// - public async Task UseItem(IGamingQueue queue, Character character, List enemys, List teammates) + public async Task UseItem(IGamingQueue queue, Character character, DecisionPoints dp, List enemys, List teammates) { bool cancel = false; bool used = false; @@ -327,7 +328,7 @@ namespace Milimoe.FunGame.Core.Entity 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); + used = await queue.UseItemAsync(this, character, dp, enemys, teammates, castRange); } if (used) { diff --git a/Entity/Skill/Effect.cs b/Entity/Skill/Effect.cs index 9292f49..b4f20a1 100644 --- a/Entity/Skill/Effect.cs +++ b/Entity/Skill/Effect.cs @@ -4,6 +4,7 @@ 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; +using Milimoe.FunGame.Core.Model; namespace Milimoe.FunGame.Core.Entity { @@ -583,6 +584,7 @@ namespace Milimoe.FunGame.Core.Entity /// 行动开始前,指定角色的行动,而不是使用顺序表自带的逻辑;或者修改对应的操作触发概率 /// /// + /// /// /// /// @@ -591,7 +593,7 @@ namespace Milimoe.FunGame.Core.Entity /// /// /// - public virtual CharacterActionType AlterActionTypeBeforeAction(Character character, CharacterState state, ref bool canUseItem, ref bool canCastSkill, ref double pUseItem, ref double pCastSkill, ref double pNormalAttack, ref bool forceAction) + public virtual CharacterActionType AlterActionTypeBeforeAction(Character character, DecisionPoints dp, CharacterState state, ref bool canUseItem, ref bool canCastSkill, ref double pUseItem, ref double pCastSkill, ref double pNormalAttack, ref bool forceAction) { return CharacterActionType.None; } @@ -795,6 +797,27 @@ namespace Milimoe.FunGame.Core.Entity return true; } + /// + /// 在角色行动后触发 + /// + /// + /// + /// + public virtual void OnCharacterActionTaken(Character actor, DecisionPoints dp, CharacterActionType type) + { + + } + + /// + /// 在角色回合决策结束后触发 + /// + /// + /// + public virtual void OnCharacterDecisionCompleted(Character actor, DecisionPoints dp) + { + + } + /// /// 对敌人造成技能伤害 [ 强烈建议使用此方法造成伤害而不是自行调用 ] /// @@ -1073,13 +1096,7 @@ namespace Milimoe.FunGame.Core.Entity /// /// /// - public void RecordCharacterApplyEffects(Character character, params List types) - { - if (GamingQueue?.LastRound.ApplyEffects.TryAdd(character, types) ?? false) - { - GamingQueue?.LastRound.ApplyEffects[character].AddRange(types); - } - } + public void RecordCharacterApplyEffects(Character character, params List types) => GamingQueue?.LastRound.AddApplyEffects(character, types); /// /// 返回特效详情 diff --git a/Entity/Skill/NormalAttack.cs b/Entity/Skill/NormalAttack.cs index a0c873f..cea38e5 100644 --- a/Entity/Skill/NormalAttack.cs +++ b/Entity/Skill/NormalAttack.cs @@ -16,7 +16,7 @@ namespace Milimoe.FunGame.Core.Entity /// /// 普通攻击说明 /// - public string Description => $"对目标敌人造成 {BaseDamageMultiplier * 100:0.##}% 攻击力 [ {Damage:0.##} ] 点{(IsMagic ? CharacterSet.GetMagicDamageName(MagicType) : "物理伤害")}。"; + public string Description => $"{(_isMagicByWeapon ? "已由武器附魔。" : "")}对目标敌人造成 {BaseDamageMultiplier * 100:0.##}% 攻击力 [ {Damage:0.##} ] 点{(IsMagic ? CharacterSet.GetMagicDamageName(MagicType) : "物理伤害")}。"; /// /// 普通攻击的通用说明 @@ -425,7 +425,16 @@ namespace Milimoe.FunGame.Core.Entity if (type == WeaponType.Talisman || type == WeaponType.Staff) { _isMagic = true; + _isMagicByWeapon = true; } + else + { + _isMagicByWeapon = false; + } + } + else + { + _isMagicByWeapon = false; } } if (queue != null && (past != _isMagic || pastType != _magicType)) @@ -477,6 +486,11 @@ namespace Milimoe.FunGame.Core.Entity /// private bool _isMagic = isMagic; + /// + /// 指示普通攻击是否由武器附魔 + /// + private bool _isMagicByWeapon = false; + /// /// 魔法类型 [ 生效型 ] /// diff --git a/Interface/Base/IGamingQueue.cs b/Interface/Base/IGamingQueue.cs index ea4b6c0..c441786 100644 --- a/Interface/Base/IGamingQueue.cs +++ b/Interface/Base/IGamingQueue.cs @@ -55,6 +55,11 @@ namespace Milimoe.FunGame.Core.Interface.Base /// public Dictionary CharacterStatistics { get; } + /// + /// 角色的决策点 + /// + public Dictionary CharacterDecisionPoints { get; } + /// /// 游戏运行的时间 /// @@ -153,22 +158,24 @@ namespace Milimoe.FunGame.Core.Interface.Base /// 使用物品 /// /// - /// + /// + /// /// /// /// /// /// - public Task UseItemAsync(Item item, Character caster, List enemys, List teammates, List castRange, List? desiredTargets = null); + public Task UseItemAsync(Item item, Character character, DecisionPoints dp, List enemys, List teammates, List castRange, List? desiredTargets = null); /// /// 角色移动 /// /// + /// /// /// /// - public Task CharacterMoveAsync(Character character, Grid target, Grid? startGrid); + public Task CharacterMoveAsync(Character character, DecisionPoints dp, Grid target, Grid? startGrid); /// /// 选取移动目标 diff --git a/Library/Common/JsonConverter/RoundRecordConverter.cs b/Library/Common/JsonConverter/RoundRecordConverter.cs index d760343..ee175f5 100644 --- a/Library/Common/JsonConverter/RoundRecordConverter.cs +++ b/Library/Common/JsonConverter/RoundRecordConverter.cs @@ -3,7 +3,6 @@ using Milimoe.FunGame.Core.Api.Utility; using Milimoe.FunGame.Core.Entity; using Milimoe.FunGame.Core.Library.Common.Architecture; using Milimoe.FunGame.Core.Library.Constant; -using Milimoe.FunGame.Core.Model; namespace Milimoe.FunGame.Core.Library.Common.JsonConverter { @@ -25,8 +24,11 @@ namespace Milimoe.FunGame.Core.Library.Common.JsonConverter result.Actor = NetworkUtility.JsonDeserialize(ref reader, options) ?? Factory.GetCharacter(); break; case nameof(RoundRecord.Targets): - List targets = NetworkUtility.JsonDeserialize>(ref reader, options) ?? []; - result.Targets.AddRange(targets); + Dictionary> targets = NetworkUtility.JsonDeserialize>>(ref reader, options) ?? []; + foreach (CharacterActionType type in targets.Keys) + { + result.Targets[type] = targets[type]; + } break; case nameof(RoundRecord.Damages): Dictionary damagesGuid = NetworkUtility.JsonDeserialize>(ref reader, options) ?? []; @@ -40,17 +42,40 @@ namespace Milimoe.FunGame.Core.Library.Common.JsonConverter } break; - case nameof(RoundRecord.ActionType): - result.ActionType = (CharacterActionType)reader.GetInt32(); + case nameof(RoundRecord.ActionTypes): + List types = NetworkUtility.JsonDeserialize>(ref reader, options) ?? []; + foreach (CharacterActionType type in types) + { + result.ActionTypes.Add(type); + } break; - case nameof(RoundRecord.Skill): - result.Skill = NetworkUtility.JsonDeserialize(ref reader, options); + case nameof(RoundRecord.Skills): + Dictionary skills = NetworkUtility.JsonDeserialize>(ref reader, options) ?? []; + foreach (CharacterActionType type in skills.Keys) + { + result.Skills[type] = skills[type]; + } break; - case nameof(RoundRecord.SkillCost): - result.SkillCost = reader.GetString() ?? ""; + case nameof(RoundRecord.SkillsCost): + Dictionary skillsCost = NetworkUtility.JsonDeserialize>(ref reader, options) ?? []; + foreach (Skill skill in skillsCost.Keys) + { + result.SkillsCost[skill] = skillsCost[skill]; + } break; - case nameof(RoundRecord.Item): - result.Item = NetworkUtility.JsonDeserialize(ref reader, options); + case nameof(RoundRecord.Items): + Dictionary items = NetworkUtility.JsonDeserialize>(ref reader, options) ?? []; + foreach (CharacterActionType type in items.Keys) + { + result.Items[type] = items[type]; + } + break; + case nameof(RoundRecord.ItemsCost): + Dictionary itemsCost = NetworkUtility.JsonDeserialize>(ref reader, options) ?? []; + foreach (Item item in itemsCost.Keys) + { + result.ItemsCost[item] = itemsCost[item]; + } break; case nameof(RoundRecord.HasKill): result.HasKill = reader.GetBoolean(); @@ -181,12 +206,16 @@ namespace Milimoe.FunGame.Core.Library.Common.JsonConverter JsonSerializer.Serialize(writer, value.Targets, options); writer.WritePropertyName(nameof(RoundRecord.Damages)); JsonSerializer.Serialize(writer, value.Damages.ToDictionary(kv => kv.Key.Guid, kv => kv.Value), options); - writer.WriteNumber(nameof(RoundRecord.ActionType), (int)value.ActionType); - writer.WritePropertyName(nameof(RoundRecord.Skill)); - JsonSerializer.Serialize(writer, value.Skill, options); - writer.WriteString(nameof(RoundRecord.SkillCost), value.SkillCost); - writer.WritePropertyName(nameof(RoundRecord.Item)); - JsonSerializer.Serialize(writer, value.Item, options); + writer.WritePropertyName(nameof(RoundRecord.ActionTypes)); + JsonSerializer.Serialize(writer, value.ActionTypes.Select(type => (int)type), options); + writer.WritePropertyName(nameof(RoundRecord.Skills)); + JsonSerializer.Serialize(writer, value.Skills.ToDictionary(kv => kv.Key.ToString(), kv => kv.Value), options); + writer.WritePropertyName(nameof(RoundRecord.SkillsCost)); + JsonSerializer.Serialize(writer, value.SkillsCost.ToDictionary(kv => kv.Key.GetIdName(), kv => kv.Value), options); + writer.WritePropertyName(nameof(RoundRecord.Items)); + JsonSerializer.Serialize(writer, value.Items.ToDictionary(kv => kv.Key.ToString(), kv => kv.Value), options); + writer.WritePropertyName(nameof(RoundRecord.ItemsCost)); + JsonSerializer.Serialize(writer, value.ItemsCost.ToDictionary(kv => kv.Key.GetIdName(), kv => kv.Value), options); writer.WriteBoolean(nameof(RoundRecord.HasKill), value.HasKill); writer.WritePropertyName(nameof(RoundRecord.Assists)); JsonSerializer.Serialize(writer, value.Assists, options); @@ -221,7 +250,7 @@ namespace Milimoe.FunGame.Core.Library.Common.JsonConverter private static Character? FindCharacterByGuid(Guid guid, RoundRecord record) { - Character? character = record.Targets.FirstOrDefault(c => c.Guid == guid); + Character? character = record.Targets.Values.SelectMany(c => c).FirstOrDefault(c => c.Guid == guid); if (character != null) return character; if (record.Actor != null && record.Actor.Guid == guid) return record.Actor; character = record.Assists.FirstOrDefault(c => c.Guid == guid); diff --git a/Model/DecisionPoints.cs b/Model/DecisionPoints.cs new file mode 100644 index 0000000..25c112e --- /dev/null +++ b/Model/DecisionPoints.cs @@ -0,0 +1,169 @@ +using System.Text.Json.Serialization; +using Milimoe.FunGame.Core.Entity; +using Milimoe.FunGame.Core.Library.Constant; + +namespace Milimoe.FunGame.Core.Model +{ + public class DecisionPoints + { + /// + /// 所用的游戏平衡常数 + /// + [JsonIgnore] + public EquilibriumConstant GameplayEquilibriumConstant { get; set; } = General.GameplayEquilibriumConstant; + + /// + /// 当前决策点 + /// + public int CurrentDecisionPoints { get; set; } = General.GameplayEquilibriumConstant.InitialDecisionPoints; + + /// + /// 决策点上限 + /// + public int MaxDecisionPoints { get; set; } = General.GameplayEquilibriumConstant.InitialDecisionPoints; + + /// + /// 每回合决策点补充数量 + /// + public int RecoverDecisionPointsPerRound { get; set; } = General.GameplayEquilibriumConstant.RecoverDecisionPointsPerRound; + + /// + /// 本回合已补充的决策点 + /// + public int DecisionPointsRecovery { get; set; } = 0; + + /// + /// 是否释放过勇气指令 + /// + public bool CourageCommandSkill { get; set; } = false; + + /// + /// 记录本回合已使用的行动类型和次数 + /// + public Dictionary ActionTypes { get; } = []; + + /// + /// 记录本回合行动的硬直时间 + /// + public List ActionsHardnessTime { get; } = []; + + /// + /// 本回合已进行的行动次数 + /// + public int ActionsTaken { get; set; } = 0; + + // 回合内的临时决策点配额加成 + private int _tempActionQuotaNormalAttack = 0; + private int _tempActionQuotaSuperSkill = 0; + private int _tempActionQuotaSkill = 0; + private int _tempActionQuotaItem = 0; + private int _tempActionQuotaOther = 0; + + /// + /// 获取当前决策点配额 + /// + /// + /// + public int this[CharacterActionType type] + { + get + { + return type switch + { + CharacterActionType.NormalAttack => GameplayEquilibriumConstant.ActionQuotaNormalAttack + _tempActionQuotaNormalAttack, + CharacterActionType.CastSuperSkill => GameplayEquilibriumConstant.ActionQuotaSuperSkill + _tempActionQuotaSuperSkill, + CharacterActionType.CastSkill => GameplayEquilibriumConstant.ActionQuotaSkill + _tempActionQuotaSkill, + CharacterActionType.PreCastSkill => GameplayEquilibriumConstant.ActionQuotaMagic, + CharacterActionType.UseItem => GameplayEquilibriumConstant.ActionQuotaItem + _tempActionQuotaItem, + _ => GameplayEquilibriumConstant.ActionQuotaOther + _tempActionQuotaOther, + }; + } + } + + /// + /// 添加临时决策点配额 [ 回合结束时清除 ] + /// + /// + /// + public void AddTempActionQuota(CharacterActionType type, int add = 1) + { + switch (type) + { + case CharacterActionType.NormalAttack: + _tempActionQuotaNormalAttack += add; + break; + case CharacterActionType.CastSkill: + _tempActionQuotaSkill += add; + break; + case CharacterActionType.CastSuperSkill: + _tempActionQuotaSuperSkill += add; + break; + case CharacterActionType.UseItem: + _tempActionQuotaItem += add; + break; + default: + _tempActionQuotaOther += add; + break; + } + } + + /// + /// 清除临时决策点配额 + /// + public void ClearTempActionQuota() + { + _tempActionQuotaNormalAttack = 0; + _tempActionQuotaSuperSkill = 0; + _tempActionQuotaSkill = 0; + _tempActionQuotaItem = 0; + _tempActionQuotaOther = 0; + } + + /// + /// 累计行动类型和次数 + /// + /// + /// + public void AddActionType(CharacterActionType type, bool addActionTaken = true) + { + if (addActionTaken) ActionsTaken++; + if (!ActionTypes.TryAdd(type, 1)) + { + ActionTypes[type]++; + } + } + + /// + /// 判断行动类型是否达到配额 + /// + /// + public bool CheckActionTypeQuota(CharacterActionType type) + { + if (ActionTypes.TryGetValue(type, out int times)) + { + return times < this[type]; + } + return true; + } + + /// + /// 获取决策点消耗 + /// + /// + /// + /// + public int GetActionPointCost(CharacterActionType type, Skill? skill = null) + { + return type switch + { + CharacterActionType.NormalAttack => GameplayEquilibriumConstant.DecisionPointsCostNormalAttack, + CharacterActionType.PreCastSkill when skill?.SkillType == SkillType.SuperSkill => GameplayEquilibriumConstant.DecisionPointsCostSuperSkill, + CharacterActionType.PreCastSkill when skill?.SkillType == SkillType.Skill => GameplayEquilibriumConstant.DecisionPointsCostSkill, + CharacterActionType.PreCastSkill when skill?.SkillType == SkillType.Magic => GameplayEquilibriumConstant.DecisionPointsCostMagic, + CharacterActionType.UseItem => GameplayEquilibriumConstant.DecisionPointsCostItem, + CharacterActionType.CastSuperSkill => GameplayEquilibriumConstant.DecisionPointsCostSuperSkillOutOfTurn, // 回合外使用爆发技 + _ => GameplayEquilibriumConstant.DecisionPointsCostOther + }; + } + } +} diff --git a/Model/EquilibriumConstant.cs b/Model/EquilibriumConstant.cs index 5769138..7fb8646 100644 --- a/Model/EquilibriumConstant.cs +++ b/Model/EquilibriumConstant.cs @@ -540,6 +540,86 @@ namespace Milimoe.FunGame.Core.Model /// public int RoleMOV_Medic { get; set; } = 3; + /// + /// 初始决策点数 + /// + public int InitialDecisionPoints { get; set; } = 1; + + /// + /// 决策点上限 + /// + public int MaxDecisionPoints { get; set; } = 7; + + /// + /// 每回合决策点补充数量 + /// + public int RecoverDecisionPointsPerRound { get; set; } = 1; + + /// + /// 每回合普通攻击决策配额 + /// + public int ActionQuotaNormalAttack { get; set; } = 1; + + /// + /// 每回合战技决策配额 + /// + public int ActionQuotaSkill { get; set; } = 1; + + /// + /// 每回合爆发技决策配额 + /// + public int ActionQuotaSuperSkill { get; set; } = 1; + + /// + /// 每回合魔法决策配额 [ 使用魔法因为会进入吟唱态,所以无论是否设置都没意义 ] + /// + public int ActionQuotaMagic { get; set; } = 1; + + /// + /// 每回合使用物品决策配额 + /// + public int ActionQuotaItem { get; set; } = 1; + + /// + /// 每回合其他决策配额 + /// + public int ActionQuotaOther { get; set; } = 0; + + /// + /// 普通攻击决策点消耗 + /// + public int DecisionPointsCostNormalAttack { get; set; } = 1; + + /// + /// 战技决策点消耗 + /// + public int DecisionPointsCostSkill { get; set; } = 2; + + /// + /// 爆发技决策点消耗(回合内) + /// + public int DecisionPointsCostSuperSkill { get; set; } = 2; + + /// + /// 爆发技决策点消耗(回合外) + /// + public int DecisionPointsCostSuperSkillOutOfTurn { get; set; } = 3; + + /// + /// 魔法决策点消耗 + /// + public int DecisionPointsCostMagic { get; set; } = 2; + + /// + /// 使用物品决策点消耗 + /// + public int DecisionPointsCostItem { get; set; } = 1; + + /// + /// 其他决策点消耗 + /// + public int DecisionPointsCostOther { get; set; } = 1; + /// /// 应用此游戏平衡常数给实体 /// diff --git a/Model/GamingQueue.cs b/Model/GamingQueue.cs index 75a44af..abc34d2 100644 --- a/Model/GamingQueue.cs +++ b/Model/GamingQueue.cs @@ -5,6 +5,7 @@ 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; +using Milimoe.FunGame.Core.Model.PrefabricatedEntity; namespace Milimoe.FunGame.Core.Model { @@ -26,6 +27,11 @@ namespace Milimoe.FunGame.Core.Model /// public Action WriteLine { get; } + /// + /// 调试模式 + /// + public bool IsDebug { get; set; } = false; + /// /// 参与本次游戏的所有角色列表 /// @@ -152,6 +158,11 @@ namespace Milimoe.FunGame.Core.Model /// public GameMap? Map => _map; + /// + /// 角色的决策点 + /// + public Dictionary CharacterDecisionPoints => _decisionPoints; + #endregion #region 保护变量 @@ -261,6 +272,11 @@ namespace Milimoe.FunGame.Core.Model /// protected Func> _factoryRoundRewardEffects = id => []; + /// + /// 角色的决策点 + /// + protected readonly Dictionary _decisionPoints = []; + /// /// 最多被插队次数,-1 为默认,即队列长度,最少为 5 /// @@ -691,19 +707,19 @@ namespace Milimoe.FunGame.Core.Model { character.HP += reallyReHP; character.MP += reallyReMP; - WriteLine($"角色 {character.Name} 回血:{recoveryHP:0.##} [{character.HP:0.##} / {character.MaxHP:0.##}] / 回蓝:{recoveryMP:0.##} [{character.MP:0.##} / {character.MaxMP:0.##}] / 当前能量:{character.EP:0.##}"); + if (IsDebug) WriteLine($"角色 {character.Name} 回血:{recoveryHP:0.##} [{character.HP:0.##} / {character.MaxHP:0.##}] / 回蓝:{recoveryMP:0.##} [{character.MP:0.##} / {character.MaxMP:0.##}] / 当前能量:{character.EP:0.##}"); } else { if (reallyReHP > 0) { character.HP += reallyReHP; - WriteLine($"角色 {character.Name} 回血:{recoveryHP:0.##} [{character.HP:0.##} / {character.MaxHP:0.##}] / 当前能量:{character.EP:0.##}"); + if (IsDebug) WriteLine($"角色 {character.Name} 回血:{recoveryHP:0.##} [{character.HP:0.##} / {character.MaxHP:0.##}] / 当前能量:{character.EP:0.##}"); } if (reallyReMP > 0) { character.MP += reallyReMP; - WriteLine($"角色 {character.Name} 回蓝:{recoveryMP:0.##} [{character.MP:0.##} / {character.MaxMP:0.##}] / 当前能量:{character.EP:0.##}"); + if (IsDebug) WriteLine($"角色 {character.Name} 回蓝:{recoveryMP:0.##} [{character.MP:0.##} / {character.MaxMP:0.##}] / 当前能量:{character.EP:0.##}"); } } @@ -748,12 +764,6 @@ namespace Milimoe.FunGame.Core.Model effect.OnEffectGained(character); } - // 如果特效具备临时驱散或者持续性驱散的功能 - if (effect.Source != null && (effect.EffectType == EffectType.WeakDispelling || effect.EffectType == EffectType.StrongDispelling)) - { - effect.Dispel(effect.Source, character, !IsTeammate(character, effect.Source) && character != effect.Source); - } - // 自身被动不会考虑 if (effect.EffectType == EffectType.None && effect.Skill.SkillType == SkillType.Passive) { @@ -785,6 +795,14 @@ namespace Milimoe.FunGame.Core.Model } } + // 如果特效具备临时驱散或者持续性驱散的功能 + effects = [.. character.Effects.Where(e => e.Source != null && (e.EffectType == EffectType.WeakDispelling || e.EffectType == EffectType.StrongDispelling))]; + foreach (Effect effect in effects) + { + if (effect.Source is null) continue; + effect.Dispel(effect.Source, character, !IsTeammate(character, effect.Source) && character != effect.Source); + } + _eliminated.Remove(character); } @@ -836,6 +854,9 @@ namespace Milimoe.FunGame.Core.Model return _isGameEnd; } + // 决策点补充 + DecisionPoints dp = DecisionPointsRecovery(character); + // 获取回合奖励 List rewards = GetRoundRewards(TotalRound, character); @@ -845,23 +866,16 @@ namespace Milimoe.FunGame.Core.Model // 队友列表 List allTeammates = GetTeammates(character); - List selectableTeammates = [.. allTeammates.Where(_queue.Contains)]; // 敌人列表 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 && - ((s.SkillType == SkillType.SuperSkill || s.SkillType == SkillType.Skill) && s.RealEPCost <= character.EP || s.SkillType == SkillType.Magic && s.RealMPCost <= character.MP))]; - - // 物品列表 - List items = [.. character.Items.Where(i => i.IsActive && i.Skills.Active != null && i.Enable && i.IsInGameItem && - i.Skills.Active.SkillType == SkillType.Item && i.Skills.Active.Enable && !i.Skills.Active.IsInEffect && i.Skills.Active.CurrentCD == 0 && i.Skills.Active.RealMPCost <= character.MP && i.Skills.Active.RealEPCost <= character.EP)]; + // 取得可选列表 + (List selectableTeammates, List selectableEnemys, List skills, List items) = GetTurnStartNeedyList(character, allTeammates, allEnemys); // 回合开始事件,允许事件返回 false 接管回合操作 // 如果事件全程接管回合操作,需要注意触发特效 - if (!await OnTurnStartAsync(character, selectableEnemys, selectableTeammates, skills, items)) + if (!await OnTurnStartAsync(character, dp, selectableEnemys, selectableTeammates, skills, items)) { _isInRound = false; return _isGameEnd; @@ -878,400 +892,398 @@ namespace Milimoe.FunGame.Core.Model effect.OnTurnStart(character, selectableEnemys, selectableTeammates, skills, items); } - // 此变量用于在取消选择时,能够重新行动 - bool decided = false; - // 最大取消次数 - int cancelTimes = 3; - // 此变量指示角色是否移动 - bool moved = false; - - // AI 决策控制器,适用于启用战棋地图的情况 - AIController? ai = null; - // 角色的起始地点,确保角色该回合移动的范围不超过 MOV Grid? startGrid = null; + // 可移动的格子列表 List canMoveGrids = []; - // 并且要筛选最远可选取角色 - List canAttackGridsByStartGrid = []; - List canCastGridsByStartGrid = []; if (_map != null) { startGrid = _map.GetCharacterCurrentGrid(character); - if (startGrid != null) { canMoveGrids = _map.GetGridsByRange(startGrid, character.MOV, false); + } + } + + // 作出了什么行动 + CharacterActionType type = CharacterActionType.None; + + // 是否结束回合 + bool endTurn = false; + bool isAI = CharactersInAI.Contains(character); + + // 循环条件:未结束回合、决策点大于0(AI控制下为0时自动结束)或角色处于吟唱态 + while (!endTurn && (!isAI || dp.CurrentDecisionPoints > 0 || character.CharacterState == CharacterState.Casting || character.CharacterState == CharacterState.PreCastSuperSkill)) + { + // 刷新可选列表 + (selectableTeammates, selectableEnemys, skills, items) = GetTurnStartNeedyList(character, allTeammates, allEnemys); + + // 并且要筛选最远可选取角色 + List canAttackGridsByStartGrid = []; + List canCastGridsByStartGrid = []; + if (_map != null && startGrid != null) + { 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)); } + allEnemys = [.. allEnemys.Where(canAttackGridsByStartGrid.Union(canCastGridsByStartGrid).SelectMany(g => g.Characters).Contains)]; + allTeammates = [.. allTeammates.Where(canAttackGridsByStartGrid.Union(canCastGridsByStartGrid).SelectMany(g => g.Characters).Contains)]; } - allEnemys = [.. allEnemys.Where(canAttackGridsByStartGrid.Union(canCastGridsByStartGrid).SelectMany(g => g.Characters).Contains)]; - allTeammates = [.. allTeammates.Where(canAttackGridsByStartGrid.Union(canCastGridsByStartGrid).SelectMany(g => g.Characters).Contains)]; - } + // 此变量用于在取消选择时,能够重新行动 + bool decided = false; + // 最大取消次数 + int cancelTimes = 3; + // 此变量指示角色是否移动 + bool moved = false; - // 作出了什么行动 - CharacterActionType type = CharacterActionType.None; + // AI 决策控制器,适用于启用战棋地图的情况 + AIController? ai = null; - // 循环条件: - // AI 控制下:未决策、取消次数大于0 - // 手动控制下:未决策 - 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) + // 循环条件: + // AI 控制下:未决策、取消次数大于0 + // 手动控制下:未决策 + isAI = CharactersInAI.Contains(character); + while (!decided && (!isAI || cancelTimes > 0)) { - if (isAI) + // 根据当前位置,更新可选取角色列表 + Grid? realGrid = null; + List canAttackGrids = []; + List canCastGrids = []; + List willMoveGridWithSkill = []; + List enemys = []; + List teammates = []; + if (_map != null) { - 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) + if (isAI) { - canCastGrids.AddRange(_map.GetGridsByRange(realGrid, skill.CastRange, true)); + 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 = [.. selectableTeammates.Where(canAttackGrids.Union(canCastGrids).SelectMany(g => g.Characters).Contains)]; + willMoveGridWithSkill = [.. canMoveGrids.Where(g => canAttackGrids.Union(canCastGrids).Contains(g))]; + } + else + { + enemys = selectableEnemys; + teammates = selectableTeammates; } - enemys = [.. selectableEnemys.Where(canAttackGrids.Union(canCastGrids).SelectMany(g => g.Characters).Contains)]; - teammates = [.. selectableTeammates.Where(canAttackGrids.Union(canCastGrids).SelectMany(g => g.Characters).Contains)]; - willMoveGridWithSkill = [.. canMoveGrids.Where(g => canAttackGrids.Union(canCastGrids).Contains(g))]; - } - else - { - enemys = selectableEnemys; - teammates = selectableTeammates; - } + // AI 决策结果(适用于启用战棋地图的情况) + AIDecision? aiDecision = null; - // 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; - - // 使用物品和释放技能、使用普通攻击的概率 - double pUseItem = 0.33; - double pCastSkill = 0.33; - double pNormalAttack = 0.34; - - // 是否强制执行(跳过状态检查等) - bool forceAction = false; - - // 不允许在吟唱和预释放状态下,修改角色的行动 - if (character.CharacterState != CharacterState.Casting && character.CharacterState != CharacterState.PreCastSuperSkill) - { - CharacterActionType actionTypeTemp = CharacterActionType.None; + // 行动开始前,可以修改可选取的角色列表 + Dictionary continuousKillingTemp = new(_continuousKilling); + Dictionary earnedMoneyTemp = new(_earnedMoney); effects = [.. character.Effects.Where(e => e.IsInEffect)]; foreach (Effect effect in effects) { - actionTypeTemp = effect.AlterActionTypeBeforeAction(character, character.CharacterState, ref canUseItem, ref canCastSkill, ref pUseItem, ref pCastSkill, ref pNormalAttack, ref forceAction); + effect.AlterSelectListBeforeAction(character, enemys, teammates, skills, continuousKillingTemp, earnedMoneyTemp); } - if (actionTypeTemp != CharacterActionType.None && actionTypeTemp != CharacterActionType.CastSkill && actionTypeTemp != CharacterActionType.CastSuperSkill) - { - type = actionTypeTemp; - } - } - if (type == CharacterActionType.None) - { - if (character.CharacterState != CharacterState.NotActionable && character.CharacterState != CharacterState.Casting && character.CharacterState != CharacterState.PreCastSuperSkill) - { - // 根据角色状态,设置一些参数 - if (character.CharacterState == CharacterState.Actionable) - { - // 可以任意行动 - if (canUseItem && canCastSkill) - { - // 不做任何处理 - } - else if (canUseItem && !canCastSkill) - { - pCastSkill = 0; - } - else if (!canUseItem && canCastSkill) - { - pUseItem = 0; - } - else - { - pUseItem = 0; - pCastSkill = 0; - } - } - else if (character.CharacterState == CharacterState.ActionRestricted) - { - // 行动受限,只能使用消耗品 - items = [.. items.Where(i => i.ItemType == ItemType.Consumable)]; - canUseItem = items.Count > 0; - if (canUseItem) - { - pCastSkill = 0; - pNormalAttack = 0; - } - else - { - pUseItem = 0; - pCastSkill = 0; - pNormalAttack = 0; - } - } - else if (character.CharacterState == CharacterState.BattleRestricted) - { - // 战斗不能,只能对自己使用物品 - enemys.Clear(); - teammates.Clear(); - skills.Clear(); - if (canUseItem) - { - pCastSkill = 0; - pNormalAttack = 0; - } - else - { - pUseItem = 0; - pCastSkill = 0; - pNormalAttack = 0; - } - } - else if (character.CharacterState == CharacterState.SkillRestricted) - { - // 技能受限,无法使用技能,可以普通攻击,可以使用物品 - skills.Clear(); - if (canUseItem) - { - pCastSkill = 0; - } - else - { - pUseItem = 0; - pCastSkill = 0; - } - } - else if (character.CharacterState == CharacterState.AttackRestricted) - { - // 攻击受限,无法普通攻击,可以使用技能,可以使用物品 - pNormalAttack = 0; - if (!canUseItem) - { - pUseItem = 0; - } - } + // 这里筛掉重复角色 + enemys = [.. enemys.Distinct()]; + teammates = [.. teammates.Distinct()]; - // 启用战棋地图时的专属 AI 决策方法 - if (isAI && ai != null && startGrid != null) - { - aiDecision = await ai.DecideAIActionAsync(character, startGrid, canMoveGrids, skills, items, allEnemys, allTeammates, enemys, teammates); - type = aiDecision.ActionType; - } - else - { - // 模组可以通过此事件来决定角色的行动 - type = await OnDecideActionAsync(character, enemys, teammates, skills, items); - } - // 若事件未完成决策,则将通过概率对角色进行自动化决策 - if (type == CharacterActionType.None) - { - type = GetActionType(pUseItem, pCastSkill, pNormalAttack); - } - } - else if (character.CharacterState == CharacterState.Casting) - { - // 如果角色上一次吟唱了魔法,这次的行动则是结算这个魔法 - type = CharacterActionType.CastSkill; - } - else if (character.CharacterState == CharacterState.PreCastSuperSkill) - { - // 角色使用回合外爆发技插队 - type = CharacterActionType.CastSuperSkill; - } - else - { - // 完全行动不能 - type = CharacterActionType.None; - } - } + baseTime = 0; + if (moved) moved = false; + else cancelTimes--; + type = CharacterActionType.None; - if (aiDecision != null && aiDecision.ActionType != CharacterActionType.Move && aiDecision.TargetMoveGrid != null) - { - // 不是纯粹移动的情况,需要手动移动 - moved = await CharacterMoveAsync(character, aiDecision.TargetMoveGrid, startGrid); - } + // 是否能使用物品和释放技能 + bool canUseItem = items.Count > 0; + bool canCastSkill = skills.Count > 0; - if (type == CharacterActionType.Move) - { - if (_map != null) + // 使用物品和释放技能、使用普通攻击的概率 + double pUseItem = 0.33; + double pCastSkill = 0.33; + double pNormalAttack = 0.34; + + // 是否强制执行(跳过状态检查等) + bool forceAction = false; + + // 不允许在吟唱和预释放状态下,修改角色的行动 + if (character.CharacterState != CharacterState.Casting && character.CharacterState != CharacterState.PreCastSuperSkill) { - Grid target; - if (aiDecision != null && aiDecision.TargetMoveGrid != null) + CharacterActionType actionTypeTemp = CharacterActionType.None; + effects = [.. character.Effects.Where(e => e.IsInEffect)]; + foreach (Effect effect in effects) { - target = aiDecision.TargetMoveGrid; - } - else - { - target = await SelectTargetGridAsync(character, enemys, teammates, _map, canMoveGrids); - } - moved = await CharacterMoveAsync(character, target, startGrid); - } - if (isAI && (aiDecision?.IsPureMove ?? false)) - { - // 取消 AI 的移动 - SetOnlyMoveHardnessTime(character, ref baseTime); - type = CharacterActionType.EndTurn; - decided = true; - WriteLine($"[ {character} ] 结束了回合!"); - await OnCharacterDoNothingAsync(character); - } - } - else if (type == CharacterActionType.NormalAttack) - { - if (!forceAction && (character.CharacterState == CharacterState.NotActionable || - character.CharacterState == CharacterState.ActionRestricted || - character.CharacterState == CharacterState.BattleRestricted || - character.CharacterState == CharacterState.AttackRestricted)) - { - WriteLine($"角色 [ {character} ] 状态为:{CharacterSet.GetCharacterState(character.CharacterState)},无法使用普通攻击!"); - } - else - { - // 使用普通攻击逻辑 - List targets; - if (aiDecision != null) - { - targets = aiDecision.Targets; - } - else - { - List attackRange = []; - if (_map != null && realGrid != null) + bool force = false; + CharacterActionType forceType = effect.AlterActionTypeBeforeAction(character, dp, character.CharacterState, ref canUseItem, ref canCastSkill, ref pUseItem, ref pCastSkill, ref pNormalAttack, ref force); + if (force && forceType != CharacterActionType.None) { - 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]; - decided = true; - - await OnCharacterNormalAttackAsync(character, targets); - - character.NormalAttack.Attack(this, character, targets); - baseTime += character.NormalAttack.RealHardnessTime; - effects = [.. character.Effects.Where(e => e.IsInEffect)]; - foreach (Effect effect in effects) - { - effect.AlterHardnessTimeAfterNormalAttack(character, ref baseTime, ref isCheckProtected); + forceAction = true; + actionTypeTemp = forceType; + break; } } + if (actionTypeTemp != CharacterActionType.None && actionTypeTemp != CharacterActionType.CastSkill && actionTypeTemp != CharacterActionType.CastSuperSkill) + { + type = actionTypeTemp; + } } - } - else if (type == CharacterActionType.PreCastSkill) - { - if (!forceAction && (character.CharacterState == CharacterState.NotActionable || - character.CharacterState == CharacterState.ActionRestricted || - character.CharacterState == CharacterState.BattleRestricted || - character.CharacterState == CharacterState.SkillRestricted)) + + if (type == CharacterActionType.None) { - WriteLine($"角色 [ {character} ] 状态为:{CharacterSet.GetCharacterState(character.CharacterState)},无法释放技能!"); - } - else - { - // 预使用技能,即开始吟唱逻辑 - Skill? skill; - if (aiDecision != null && aiDecision.SkillToUse is Skill s) + if (character.CharacterState != CharacterState.NotActionable && character.CharacterState != CharacterState.Casting && character.CharacterState != CharacterState.PreCastSuperSkill) { - 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)]; - } - if (skill != null) - { - // 吟唱前需要先选取目标 - if (skill.SkillType == SkillType.Magic) + // 根据角色状态,设置一些参数 + if (character.CharacterState == CharacterState.Actionable) { - List targets; - if (aiDecision != null) + // 可以任意行动 + if (canUseItem && canCastSkill) { - targets = aiDecision.Targets; + // 不做任何处理 + } + else if (canUseItem && !canCastSkill) + { + pCastSkill = 0; + } + else if (!canUseItem && canCastSkill) + { + pUseItem = 0; } 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); + pUseItem = 0; + pCastSkill = 0; } - if (targets.Count > 0) + } + else if (character.CharacterState == CharacterState.ActionRestricted) + { + // 行动受限,只能使用消耗品 + items = [.. items.Where(i => i.ItemType == ItemType.Consumable)]; + canUseItem = items.Count > 0; + if (canUseItem) { - // 免疫检定 - await CheckSkilledImmuneAsync(character, targets, skill); - - if (targets.Count > 0) - { - LastRound.Targets = [.. targets]; - decided = true; - - character.CharacterState = CharacterState.Casting; - SkillTarget skillTarget = new(skill, targets); - await OnCharacterPreCastSkillAsync(character, skillTarget); - - _castingSkills[character] = skillTarget; - baseTime += skill.RealCastTime; - isCheckProtected = false; - skill.OnSkillCasting(this, character, targets); - } + pCastSkill = 0; + pNormalAttack = 0; } + else + { + pUseItem = 0; + pCastSkill = 0; + pNormalAttack = 0; + } + } + else if (character.CharacterState == CharacterState.BattleRestricted) + { + // 战斗不能,只能对自己使用物品 + enemys.Clear(); + teammates.Clear(); + skills.Clear(); + if (canUseItem) + { + pCastSkill = 0; + pNormalAttack = 0; + } + else + { + pUseItem = 0; + pCastSkill = 0; + pNormalAttack = 0; + } + } + else if (character.CharacterState == CharacterState.SkillRestricted) + { + // 技能受限,无法使用技能,可以普通攻击,可以使用物品 + skills.Clear(); + if (canUseItem) + { + pCastSkill = 0; + } + else + { + pUseItem = 0; + pCastSkill = 0; + } + } + else if (character.CharacterState == CharacterState.AttackRestricted) + { + // 攻击受限,无法普通攻击,可以使用技能,可以使用物品 + pNormalAttack = 0; + if (!canUseItem) + { + pUseItem = 0; + } + } + + // 启用战棋地图时的专属 AI 决策方法 + if (isAI && ai != null && startGrid != null) + { + aiDecision = await ai.DecideAIActionAsync(character, dp, startGrid, canMoveGrids, skills, items, allEnemys, allTeammates, enemys, teammates); + type = aiDecision.ActionType; } else { - // 只有魔法需要吟唱,战技和爆发技直接释放 - if (CheckCanCast(character, skill, out double cost)) + // 模组可以通过此事件来决定角色的行动 + type = await OnDecideActionAsync(character, dp, enemys, teammates, skills, items); + } + // 若事件未完成决策,则将通过概率对角色进行自动化决策 + if (type == CharacterActionType.None) + { + type = GetActionType(dp, pUseItem, pCastSkill, pNormalAttack); + } + } + else if (character.CharacterState == CharacterState.Casting) + { + // 如果角色上一次吟唱了魔法,这次的行动则是结算这个魔法 + type = CharacterActionType.CastSkill; + } + else if (character.CharacterState == CharacterState.PreCastSuperSkill) + { + // 角色使用回合外爆发技插队 + type = CharacterActionType.CastSuperSkill; + } + else + { + // 完全行动不能 + type = CharacterActionType.None; + } + } + + if (aiDecision != null && aiDecision.ActionType != CharacterActionType.Move && aiDecision.TargetMoveGrid != null) + { + // 不是纯粹移动的情况,需要手动移动 + moved = await CharacterMoveAsync(character, dp, aiDecision.TargetMoveGrid, startGrid); + } + + int costDP = dp.GetActionPointCost(type); + + 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, dp, target, startGrid); + } + if (isAI && (aiDecision?.IsPureMove ?? false)) + { + // 取消 AI 的移动 + SetOnlyMoveHardnessTime(character, dp, ref baseTime); + type = CharacterActionType.EndTurn; + decided = true; + endTurn = true; + WriteLine($"[ {character} ] 结束了回合!"); + await OnCharacterDoNothingAsync(character, dp); + } + } + else if (type == CharacterActionType.NormalAttack) + { + if (!forceAction && (character.CharacterState == CharacterState.NotActionable || + character.CharacterState == CharacterState.ActionRestricted || + character.CharacterState == CharacterState.BattleRestricted || + character.CharacterState == CharacterState.AttackRestricted)) + { + if (IsDebug) WriteLine($"角色 [ {character} ] 状态为:{CharacterSet.GetCharacterState(character.CharacterState)},无法使用普通攻击!"); + } + else if (dp.CurrentDecisionPoints < costDP) + { + if (IsDebug) WriteLine($"角色 [ {character} ] 决策点不足,无法使用普通攻击!"); + } + else if (!dp.CheckActionTypeQuota(CharacterActionType.NormalAttack)) + { + if (IsDebug) WriteLine($"角色 [ {character} ] 该回合使用普通攻击的次数已超过决策点配额,无法再次使用普通攻击!"); + } + else + { + // 使用普通攻击逻辑 + 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[CharacterActionType.NormalAttack] = [.. targets]; + LastRound.ActionTypes.Add(CharacterActionType.NormalAttack); + dp.AddActionType(CharacterActionType.NormalAttack); + dp.CurrentDecisionPoints -= costDP; + decided = true; + + await OnCharacterNormalAttackAsync(character, dp, targets); + + character.NormalAttack.Attack(this, character, targets); + baseTime += character.NormalAttack.RealHardnessTime; + effects = [.. character.Effects.Where(e => e.IsInEffect)]; + foreach (Effect effect in effects) + { + effect.AlterHardnessTimeAfterNormalAttack(character, ref baseTime, ref isCheckProtected); + } + } + } + } + else if (type == CharacterActionType.PreCastSkill) + { + if (!forceAction && (character.CharacterState == CharacterState.NotActionable || + character.CharacterState == CharacterState.ActionRestricted || + character.CharacterState == CharacterState.BattleRestricted || + character.CharacterState == CharacterState.SkillRestricted)) + { + if (IsDebug) WriteLine($"角色 [ {character} ] 状态为:{CharacterSet.GetCharacterState(character.CharacterState)},无法释放技能!"); + } + else + { + // 预使用技能,即开始吟唱逻辑 + 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)]; + } + if (skill != null) + { + costDP = dp.GetActionPointCost(type, skill); + if (dp.CurrentDecisionPoints < costDP) + { + if (IsDebug) WriteLine($"角色 [ {character} ] 决策点不足,无法释放技能!"); + } + else if (skill.SkillType == SkillType.Magic) + { + // 吟唱前需要先选取目标 List targets; if (aiDecision != null) { @@ -1295,78 +1307,208 @@ namespace Milimoe.FunGame.Core.Model if (targets.Count > 0) { - LastRound.Targets = [.. targets]; + LastRound.Skills[CharacterActionType.PreCastSkill] = skill; + LastRound.Targets[CharacterActionType.PreCastSkill] = [.. targets]; + LastRound.ActionTypes.Add(CharacterActionType.PreCastSkill); + dp.AddActionType(CharacterActionType.PreCastSkill); + dp.CurrentDecisionPoints -= costDP; decided = true; + endTurn = true; + character.CharacterState = CharacterState.Casting; SkillTarget skillTarget = new(skill, targets); - await OnCharacterPreCastSkillAsync(character, skillTarget); + await OnCharacterPreCastSkillAsync(character, dp, skillTarget); + _castingSkills[character] = skillTarget; + baseTime += skill.RealCastTime; + isCheckProtected = false; skill.OnSkillCasting(this, character, targets); - skill.BeforeSkillCasted(); - - character.EP -= cost; - baseTime += skill.RealHardnessTime; - skill.CurrentCD = skill.RealCD; - skill.Enable = false; - LastRound.SkillCost = $"{-cost:0.##} EP"; - WriteLine($"[ {character} ] 消耗了 {cost:0.##} 点能量,释放了{(skill.IsSuperSkill ? "爆发技" : "战技")} [ {skill.Name} ]!{(skill.Slogan != "" ? skill.Slogan : "")}"); - - await OnCharacterCastSkillAsync(character, skillTarget, cost); - - skill.OnSkillCasted(this, character, targets); - effects = [.. character.Effects.Where(e => e.IsInEffect)]; - foreach (Effect effect in effects) + } + } + } + else if (skill is CourageCommandSkill && dp.CourageCommandSkill) + { + if (IsDebug) WriteLine($"角色 [ {character} ] 该回合已经使用过勇气指令,无法再次使用勇气指令!"); + } + else if (skill is not CourageCommandSkill && !skill.IsSuperSkill && !dp.CheckActionTypeQuota(CharacterActionType.CastSkill)) + { + if (IsDebug) WriteLine($"角色 [ {character} ] 该回合使用战技的次数已超过决策点配额,无法再次使用战技!"); + } + else if (skill is not CourageCommandSkill && skill.IsSuperSkill && !dp.CheckActionTypeQuota(CharacterActionType.CastSuperSkill)) + { + if (IsDebug) WriteLine($"角色 [ {character} ] 该回合使用爆发技的次数已超过决策点配额,无法再次使用爆发技!"); + } + else + { + // 只有魔法需要吟唱,战技和爆发技直接释放 + if (CheckCanCast(character, skill, out double cost)) + { + List targets; + if (aiDecision != null) + { + targets = aiDecision.Targets; + } + else + { + List castRange = []; + if (_map != null && realGrid != null) { - effect.AlterHardnessTimeAfterCastSkill(character, skill, ref baseTime, ref isCheckProtected); + 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) + { + // 免疫检定 + await CheckSkilledImmuneAsync(character, targets, skill); + + if (targets.Count > 0) + { + CharacterActionType skillType = skill.SkillType == SkillType.SuperSkill ? CharacterActionType.CastSuperSkill : CharacterActionType.CastSkill; + LastRound.Skills[skillType] = skill; + LastRound.Targets[skillType] = [.. targets]; + LastRound.ActionTypes.Add(skillType); + if (skill is not CourageCommandSkill) + { + dp.AddActionType(skillType); + dp.CurrentDecisionPoints -= costDP; + } + else + { + // 勇气指令不消耗决策点,但是有标记 + dp.CourageCommandSkill = true; + } + decided = true; + + SkillTarget skillTarget = new(skill, targets); + await OnCharacterPreCastSkillAsync(character, dp, skillTarget); + + skill.OnSkillCasting(this, character, targets); + skill.BeforeSkillCasted(); + + character.EP -= cost; + baseTime += skill.RealHardnessTime; + skill.CurrentCD = skill.RealCD; + skill.Enable = false; + LastRound.SkillsCost[skill] = $"{-cost:0.##} EP"; + WriteLine($"[ {character} ] 消耗了 {cost:0.##} 点能量,释放了{(skill.IsSuperSkill ? "爆发技" : "战技")} [ {skill.Name} ]!{(skill.Slogan != "" ? skill.Slogan : "")}"); + + await OnCharacterCastSkillAsync(character, dp, skillTarget, cost); + + skill.OnSkillCasted(this, character, targets); + effects = [.. character.Effects.Where(e => e.IsInEffect)]; + foreach (Effect effect in effects) + { + effect.AlterHardnessTimeAfterCastSkill(character, skill, ref baseTime, ref isCheckProtected); + } } } } } } - LastRound.Skill = skill; } } - } - else if (type == CharacterActionType.CastSkill) - { - if (_castingSkills.TryGetValue(character, out SkillTarget skillTarget)) + else if (type == CharacterActionType.CastSkill) { - // 使用技能逻辑,结束吟唱状态 - character.CharacterState = CharacterState.Actionable; - character.UpdateCharacterState(); - Skill skill = skillTarget.Skill; - List targets = [.. skillTarget.Targets.Where(c => c == character || !c.IsUnselectable)]; - - // 判断是否能够释放技能 - if (targets.Count > 0 && CheckCanCast(character, skill, out double cost)) + if (_castingSkills.TryGetValue(character, out SkillTarget skillTarget)) { - // 免疫检定 - await CheckSkilledImmuneAsync(character, targets, skill); + // 使用技能逻辑,结束吟唱状态 + character.CharacterState = CharacterState.Actionable; + character.UpdateCharacterState(); + Skill skill = skillTarget.Skill; + List targets = [.. skillTarget.Targets.Where(c => c == character || !c.IsUnselectable)]; - if (targets.Count > 0) + // 判断是否能够释放技能 + if (targets.Count > 0 && CheckCanCast(character, skill, out double cost)) { - decided = true; - LastRound.Targets = [.. targets]; - LastRound.Skill = skill; - _castingSkills.Remove(character); + // 免疫检定 + await CheckSkilledImmuneAsync(character, targets, skill); - skill.BeforeSkillCasted(); + if (targets.Count > 0) + { + decided = true; + endTurn = true; + LastRound.Targets[CharacterActionType.CastSkill] = [.. targets]; + LastRound.Skills[CharacterActionType.CastSkill] = skill; + LastRound.ActionTypes.Add(CharacterActionType.CastSkill); + _castingSkills.Remove(character); - character.MP -= cost; - baseTime += skill.RealHardnessTime; - skill.CurrentCD = skill.RealCD; - skill.Enable = false; - LastRound.SkillCost = $"{-cost:0.##} MP"; - WriteLine($"[ {character} ] 消耗了 {cost:0.##} 点魔法值,释放了魔法 [ {skill.Name} ]!{(skill.Slogan != "" ? skill.Slogan : "")}"); + skill.BeforeSkillCasted(); - await OnCharacterCastSkillAsync(character, skillTarget, cost); + character.MP -= cost; + baseTime += skill.RealHardnessTime; + skill.CurrentCD = skill.RealCD; + skill.Enable = false; + LastRound.SkillsCost[skill] = $"{-cost:0.##} MP"; + WriteLine($"[ {character} ] 消耗了 {cost:0.##} 点魔法值,释放了魔法 [ {skill.Name} ]!{(skill.Slogan != "" ? skill.Slogan : "")}"); - skill.OnSkillCasted(this, character, targets); + await OnCharacterCastSkillAsync(character, dp, skillTarget, cost); + + skill.OnSkillCasted(this, character, targets); + } + } + else + { + WriteLine($"[ {character} ] 放弃释放技能!"); + // 放弃释放技能会获得3的硬直时间 + if (baseTime == 0) baseTime = 3; + } + + effects = [.. character.Effects.Where(e => e.IsInEffect)]; + foreach (Effect effect in effects) + { + effect.AlterHardnessTimeAfterCastSkill(character, skill, ref baseTime, ref isCheckProtected); } } else { - WriteLine($"[ {character} ] 放弃释放技能!"); + // 原吟唱的技能丢失(被打断或者被取消),允许角色再次决策 + character.CharacterState = CharacterState.Actionable; + character.UpdateCharacterState(); + } + } + else if (type == CharacterActionType.CastSuperSkill) + { + dp.AddActionType(CharacterActionType.CastSuperSkill); + LastRound.ActionTypes.Add(CharacterActionType.CastSuperSkill); + decided = true; + endTurn = true; + // 结束预释放爆发技的状态 + character.CharacterState = CharacterState.Actionable; + character.UpdateCharacterState(); + Skill skill = _castingSuperSkills[character]; + LastRound.Skills[CharacterActionType.CastSuperSkill] = skill; + _castingSuperSkills.Remove(character); + + // 判断是否能够释放技能 + if (CheckCanCast(character, skill, out double cost)) + { + // 预释放的爆发技不可取消 + 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[CharacterActionType.CastSuperSkill] = [.. targets]; + + skill.BeforeSkillCasted(); + + character.EP -= cost; + baseTime += skill.RealHardnessTime; + skill.CurrentCD = skill.RealCD; + skill.Enable = false; + LastRound.SkillsCost[skill] = $"{-cost:0.##} EP"; + WriteLine($"[ {character} ] 消耗了 {cost:0.##} 点能量值,释放了爆发技 [ {skill.Name} ]!{(skill.Slogan != "" ? skill.Slogan : "")}"); + + SkillTarget skillTarget = new(skill, targets); + await OnCharacterCastSkillAsync(character, dp, skillTarget, cost); + + skill.OnSkillCasted(this, character, targets); + } + else + { + WriteLine($"[ {character} ] 因能量不足放弃释放爆发技!"); // 放弃释放技能会获得3的硬直时间 if (baseTime == 0) baseTime = 3; } @@ -1377,131 +1519,123 @@ namespace Milimoe.FunGame.Core.Model effect.AlterHardnessTimeAfterCastSkill(character, skill, ref baseTime, ref isCheckProtected); } } - else + else if (type == CharacterActionType.UseItem) { - // 原吟唱的技能丢失(被打断或者被取消),允许角色再次决策 - character.CharacterState = CharacterState.Actionable; - character.UpdateCharacterState(); - } - } - else if (type == CharacterActionType.CastSuperSkill) - { - decided = true; - // 结束预释放爆发技的状态 - character.CharacterState = CharacterState.Actionable; - character.UpdateCharacterState(); - Skill skill = _castingSuperSkills[character]; - LastRound.Skill = skill; - _castingSuperSkills.Remove(character); - - // 判断是否能够释放技能 - if (CheckCanCast(character, skill, out double cost)) - { - // 预释放的爆发技不可取消 - 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]; - - skill.BeforeSkillCasted(); - - character.EP -= cost; - baseTime += skill.RealHardnessTime; - skill.CurrentCD = skill.RealCD; - skill.Enable = false; - LastRound.SkillCost = $"{-cost:0.##} EP"; - WriteLine($"[ {character} ] 消耗了 {cost:0.##} 点能量值,释放了爆发技 [ {skill.Name} ]!{(skill.Slogan != "" ? skill.Slogan : "")}"); - - SkillTarget skillTarget = new(skill, targets); - await OnCharacterCastSkillAsync(character, skillTarget, cost); - - skill.OnSkillCasted(this, character, targets); - } - else - { - WriteLine($"[ {character} ] 因能量不足放弃释放爆发技!"); - // 放弃释放技能会获得3的硬直时间 - if (baseTime == 0) baseTime = 3; - } - - effects = [.. character.Effects.Where(e => e.IsInEffect)]; - foreach (Effect effect in effects) - { - effect.AlterHardnessTimeAfterCastSkill(character, skill, ref baseTime, ref isCheckProtected); - } - } - else if (type == CharacterActionType.UseItem) - { - // 使用物品逻辑 - 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 控制下随机选取一个物品 - item = items[Random.Shared.Next(items.Count)]; - } - if (item != null && item.Skills.Active != null) - { - Skill skill = item.Skills.Active; - List castRange = []; - if (_map != null && realGrid != null) + // 使用物品逻辑 + Item? item; + if (aiDecision != null && aiDecision.ItemToUse != 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)]; + item = aiDecision.ItemToUse; } - if (await UseItemAsync(item, character, enemys, teammates, castRange, aiDecision?.Targets)) + else { - decided = true; - LastRound.Item = item; - baseTime += skill.RealHardnessTime > 0 ? skill.RealHardnessTime : 5; - effects = [.. character.Effects.Where(e => e.IsInEffect)]; - foreach (Effect effect in effects) + item = await OnSelectItemAsync(character, items); + } + if (item is null && CharactersInAI.Contains(character) && items.Count > 0) + { + // AI 控制下随机选取一个物品 + item = items[Random.Shared.Next(items.Count)]; + } + if (item != null && item.Skills.Active != null) + { + Skill skill = item.Skills.Active; + List castRange = []; + if (_map != null && realGrid != null) { - effect.AlterHardnessTimeAfterCastSkill(character, skill, ref baseTime, ref isCheckProtected); + 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 (dp.CurrentDecisionPoints < costDP) + { + if (IsDebug) WriteLine($"角色 [ {character} ] 决策点不足,无法使用物品!"); + } + else if (!dp.CheckActionTypeQuota(CharacterActionType.UseItem)) + { + if (IsDebug) WriteLine($"角色 [ {character} ] 该回合使用物品的次数已超过决策点配额,无法再使用物品!"); + } + else if (await UseItemAsync(item, character, dp, enemys, teammates, castRange, aiDecision?.Targets)) + { + dp.AddActionType(CharacterActionType.UseItem); + dp.CurrentDecisionPoints -= costDP; + LastRound.ActionTypes.Add(CharacterActionType.UseItem); + LastRound.Items[CharacterActionType.UseItem] = item; + decided = true; + baseTime += skill.RealHardnessTime > 0 ? skill.RealHardnessTime : 5; + effects = [.. character.Effects.Where(e => e.IsInEffect)]; + foreach (Effect effect in effects) + { + effect.AlterHardnessTimeAfterCastSkill(character, skill, ref baseTime, ref isCheckProtected); + } } } } + else if (type == CharacterActionType.EndTurn) + { + SetOnlyMoveHardnessTime(character, dp, ref baseTime); + decided = true; + endTurn = true; + WriteLine($"[ {character} ] 结束了回合!"); + await OnCharacterDoNothingAsync(character, dp); + } + else + { + if (baseTime == 0) baseTime += 8; + decided = true; + endTurn = true; + WriteLine($"[ {character} ] 完全行动不能!"); + } + + if (forceAction) + { + endTurn = true; + } } - else if (type == CharacterActionType.EndTurn) + + if (!decided && (isAI || cancelTimes == 0)) { - SetOnlyMoveHardnessTime(character, ref baseTime); - decided = true; - WriteLine($"[ {character} ] 结束了回合!"); - await OnCharacterDoNothingAsync(character); + endTurn = true; + baseTime += 5; + type = CharacterActionType.EndTurn; } - else + + if (type == CharacterActionType.None) { - if (baseTime == 0) baseTime += 8; - decided = true; - WriteLine($"[ {character} ] 完全行动不能!"); + endTurn = true; + WriteLine($"[ {character} ] 放弃了行动!"); + await OnCharacterGiveUpAsync(character, dp); + } + + if (character.CharacterState != CharacterState.Casting) dp.ActionsHardnessTime.Add(baseTime); + + await OnCharacterActionTakenAsync(character, dp, type, LastRound); + + effects = [.. character.Effects.Where(e => e.IsInEffect)]; + foreach (Effect effect in effects) + { + effect.OnCharacterActionTaken(character, dp, type); + } + + if (!await AfterCharacterAction(character, type)) + { + endTurn = true; } } - if (!decided && (isAI || cancelTimes == 0)) + if (character.CharacterState != CharacterState.Casting) { - baseTime += 5; - type = CharacterActionType.EndTurn; - } - - if (type == CharacterActionType.None) - { - WriteLine($"[ {character} ] 放弃了行动!"); - await OnCharacterGiveUpAsync(character); + baseTime = dp.ActionsTaken > 1 ? (dp.ActionsHardnessTime.Max() + dp.ActionsTaken) : dp.ActionsHardnessTime.Max(); } _stats[character].ActionTurn += 1; - LastRound.ActionType = type; - await AfterCharacterAction(character, type); + await AfterCharacterDecision(character, dp); + await OnCharacterDecisionCompletedAsync(character, dp, LastRound); + effects = [.. character.Effects.Where(e => e.IsInEffect)]; + foreach (Effect effect in effects) + { + effect.OnCharacterDecisionCompleted(character, dp); + } // 统一在回合结束时处理角色的死亡 await ProcessCharacterDeathAsync(character); @@ -1512,7 +1646,7 @@ namespace Milimoe.FunGame.Core.Model if (_isGameEnd) { // 回合结束事件 - await OnTurnEndAsync(character); + await OnTurnEndAsync(character, dp); await AfterTurnAsync(character); @@ -1535,7 +1669,7 @@ namespace Milimoe.FunGame.Core.Model } AddCharacter(character, newHardnessTime, isCheckProtected); LastRound.HardnessTime = newHardnessTime; - await OnQueueUpdatedAsync(_queue, character, newHardnessTime, QueueUpdatedReason.Action, "设置角色行动后的硬直时间。"); + await OnQueueUpdatedAsync(_queue, character, dp, newHardnessTime, QueueUpdatedReason.Action, "设置角色行动后的硬直时间。"); effects = [.. character.Effects]; foreach (Effect effect in effects) @@ -1565,11 +1699,14 @@ namespace Milimoe.FunGame.Core.Model } } + // 清空临时决策点 + dp.ClearTempActionQuota(); + // 有人想要插队吗? await WillPreCastSuperSkill(); // 回合结束事件 - await OnTurnEndAsync(character); + await OnTurnEndAsync(character, dp); await AfterTurnAsync(character); @@ -1619,7 +1756,24 @@ namespace Milimoe.FunGame.Core.Model /// /// /// - protected virtual async Task AfterCharacterAction(Character character, CharacterActionType type) + /// 返回 false 结束回合 + protected virtual async Task AfterCharacterAction(Character character, CharacterActionType type) + { + List allTeammates = GetTeammates(character); + Character[] allEnemys = [.. _allCharacters.Where(c => c != character && !allTeammates.Contains(c) && !_eliminated.Contains(c))]; + if (!allEnemys.Any(c => c.HP > 0)) + { + return false; + } + return true; + } + + /// + /// 角色完成回合决策后触发 + /// + /// + /// + protected virtual async Task AfterCharacterDecision(Character character, DecisionPoints dp) { await Task.CompletedTask; } @@ -2331,6 +2485,29 @@ namespace Milimoe.FunGame.Core.Model #region 回合内-辅助方法 + /// + /// 取得回合开始时必需的列表 + /// + /// + public (List, List, List, List) GetTurnStartNeedyList(Character character, List allTeammates, List allEnemys) + { + // 可选队友列表 + List selectableTeammates = [.. allTeammates.Where(_queue.Contains)]; + + // 可选敌人列表 + 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 && + ((s.SkillType == SkillType.SuperSkill || s.SkillType == SkillType.Skill) && s.RealEPCost <= character.EP || s.SkillType == SkillType.Magic && s.RealMPCost <= character.MP))]; + + // 物品列表 + List items = [.. character.Items.Where(i => i.IsActive && i.Skills.Active != null && i.Enable && i.IsInGameItem && + i.Skills.Active.SkillType == SkillType.Item && i.Skills.Active.Enable && !i.Skills.Active.IsInEffect && i.Skills.Active.CurrentCD == 0 && i.Skills.Active.RealMPCost <= character.MP && i.Skills.Active.RealEPCost <= character.EP)]; + + return (selectableTeammates, selectableEnemys, skills, items); + } + /// /// 需要处理复活和解除施法等 /// @@ -2399,12 +2576,13 @@ namespace Milimoe.FunGame.Core.Model /// /// /// + /// /// /// /// /// /// - public async Task UseItemAsync(Item item, Character character, List enemys, List teammates, List castRange, List? desiredTargets = null) + public async Task UseItemAsync(Item item, Character character, DecisionPoints dp, List enemys, List teammates, List castRange, List? desiredTargets = null) { if (CheckCanCast(character, item, out double costMP, out double costEP)) { @@ -2428,7 +2606,7 @@ namespace Milimoe.FunGame.Core.Model if (targets.Count > 0) { - LastRound.Targets = [.. targets]; + LastRound.Targets[CharacterActionType.UseItem] = [.. targets]; WriteLine($"[ {character} ] 使用了物品 [ {item.Name} ]!"); item.ReduceTimesAndRemove(); @@ -2436,7 +2614,7 @@ namespace Milimoe.FunGame.Core.Model { character.Items.Remove(item); } - await OnCharacterUseItemAsync(character, item, targets); + await OnCharacterUseItemAsync(character, dp, item, targets); skill.OnSkillCasting(this, character, targets); skill.BeforeSkillCasted(); @@ -2448,15 +2626,15 @@ namespace Milimoe.FunGame.Core.Model if (costMP > 0) { character.MP -= costMP; - LastRound.SkillCost = $"{-costMP:0.##} MP"; + LastRound.ItemsCost[item] = $"{-costMP:0.##} MP"; line += $"消耗了 {costMP:0.##} 点魔法值,"; } if (costEP > 0) { character.EP -= costEP; - if (LastRound.SkillCost != "") LastRound.SkillCost += " / "; - LastRound.SkillCost += $"{-costEP:0.##} EP"; + if (LastRound.ItemsCost[item] != "") LastRound.ItemsCost[item] += " / "; + LastRound.ItemsCost[item] += $"{-costEP:0.##} EP"; line += $"消耗了 {costEP:0.##} 点能量,"; } @@ -2464,7 +2642,7 @@ namespace Milimoe.FunGame.Core.Model WriteLine(line); SkillTarget skillTarget = new(skill, targets); - await OnCharacterCastItemSkillAsync(character, item, skillTarget, costMP, costEP); + await OnCharacterCastItemSkillAsync(character, dp, item, skillTarget, costMP, costEP); skill.OnSkillCasted(this, character, targets); return true; @@ -2479,10 +2657,11 @@ namespace Milimoe.FunGame.Core.Model /// 角色移动实际逻辑 /// /// + /// /// /// /// - public async Task CharacterMoveAsync(Character character, Grid target, Grid? startGrid) + public async Task CharacterMoveAsync(Character character, DecisionPoints dp, Grid target, Grid? startGrid) { if (target.Id != -1) { @@ -2490,7 +2669,7 @@ namespace Milimoe.FunGame.Core.Model if (steps > 0) { WriteLine($"[ {character} ] 移动了 {steps} 步!"); - await OnCharacterMoveAsync(character, target); + await OnCharacterMoveAsync(character, dp, target); return true; } } @@ -2500,12 +2679,30 @@ namespace Milimoe.FunGame.Core.Model /// /// 通过概率计算角色要干嘛 /// + /// /// /// /// /// - public static CharacterActionType GetActionType(double pUseItem, double pCastSkill, double pNormalAttack) + public static CharacterActionType GetActionType(DecisionPoints dp, double pUseItem, double pCastSkill, double pNormalAttack) { + if (!dp.CheckActionTypeQuota(CharacterActionType.NormalAttack) || dp.CurrentDecisionPoints < dp.GameplayEquilibriumConstant.DecisionPointsCostNormalAttack) + { + pNormalAttack = 0; + } + + if (!dp.CheckActionTypeQuota(CharacterActionType.UseItem) || dp.CurrentDecisionPoints < dp.GameplayEquilibriumConstant.DecisionPointsCostItem) + { + pUseItem = 0; + } + + if (dp.CurrentDecisionPoints < dp.GameplayEquilibriumConstant.DecisionPointsCostSkill && + dp.CurrentDecisionPoints < dp.GameplayEquilibriumConstant.DecisionPointsCostSuperSkill && + dp.CurrentDecisionPoints < dp.GameplayEquilibriumConstant.DecisionPointsCostMagic) + { + pCastSkill = 0; + } + if (pUseItem == 0 && pCastSkill == 0 && pNormalAttack == 0) { return CharacterActionType.EndTurn; @@ -2974,9 +3171,12 @@ namespace Milimoe.FunGame.Core.Model /// 对角色设置仅移动的硬直时间 /// /// + /// /// - public void SetOnlyMoveHardnessTime(Character character, ref double baseTime) + public void SetOnlyMoveHardnessTime(Character character, DecisionPoints dp, ref double baseTime) { + if (dp.ActionsTaken > 0) return; + baseTime += 3; if (character.CharacterState == CharacterState.NotActionable || character.CharacterState == CharacterState.ActionRestricted || @@ -2987,6 +3187,63 @@ namespace Milimoe.FunGame.Core.Model } } + /// + /// 决策点补充 + /// + public DecisionPoints DecisionPointsRecovery(Character character) + { + DecisionPoints dp; + if (!_decisionPoints.TryGetValue(character, out DecisionPoints? value) || value is null) + { + value = new(); + _decisionPoints[character] = value; + } + dp = value; + + // 吟唱态不做处理 + if (character.CharacterState == CharacterState.Casting || character.CharacterState == CharacterState.PreCastSuperSkill) + { + return dp; + } + + // 清空上回合的记录 + dp.CourageCommandSkill = false; + dp.ActionsHardnessTime.Clear(); + dp.ActionTypes.Clear(); + dp.ActionsTaken = 0; + + // 根据角色状态补充决策点 + int pointsToAdd; + + // 每回合提升决策点上限 + if (dp.MaxDecisionPoints < dp.GameplayEquilibriumConstant.MaxDecisionPoints) + { + dp.MaxDecisionPoints++; + } + else if (dp.MaxDecisionPoints > dp.GameplayEquilibriumConstant.MaxDecisionPoints) + { + dp.MaxDecisionPoints = dp.GameplayEquilibriumConstant.MaxDecisionPoints; + } + + if (character.CharacterState == CharacterState.NotActionable || character.CharacterState == CharacterState.ActionRestricted) + { + // 完全行动不能/行动受限:补充上限1/4 + pointsToAdd = Math.Max(1, dp.MaxDecisionPoints / 4); + dp.CurrentDecisionPoints = Math.Min(dp.CurrentDecisionPoints + pointsToAdd, dp.MaxDecisionPoints); + } + else + { + // 正常状态:补充上限一半 + pointsToAdd = Math.Max(1, dp.MaxDecisionPoints / 2); + dp.CurrentDecisionPoints = Math.Min(dp.CurrentDecisionPoints + pointsToAdd, dp.MaxDecisionPoints); + } + + dp.DecisionPointsRecovery = pointsToAdd; + + if (IsDebug) WriteLine($"[ {character} ] 回合开始,补充 {pointsToAdd} 决策点,当前 {dp.CurrentDecisionPoints}/{dp.MaxDecisionPoints} 决策点。"); + return dp; + } + #endregion #region 回合奖励 @@ -3059,7 +3316,6 @@ namespace Milimoe.FunGame.Core.Model WriteLine($"[ {character} ] 获得了回合奖励!{skill.Description}".Trim()); if (skill.IsActive) { - LastRound.Targets.Add(character); skill.OnSkillCasted(this, character, [character]); } else @@ -3103,6 +3359,11 @@ namespace Milimoe.FunGame.Core.Model // 选取所有 AI 控制角色 foreach (Character other in _queue.Where(c => c.CharacterState == CharacterState.Actionable && CharactersInAI.Contains(c)).ToList()) { + if (_decisionPoints.TryGetValue(other, out DecisionPoints? dp) && dp != null && dp.CurrentDecisionPoints < dp.GetActionPointCost(CharacterActionType.CastSuperSkill)) + { + continue; + } + // 有 65% 欲望插队 if (Random.Shared.NextDouble() < 0.65) { @@ -3189,7 +3450,12 @@ namespace Milimoe.FunGame.Core.Model { _respawnTimes[character] += 1; } - await OnQueueUpdatedAsync(_queue, character, hardnessTime, QueueUpdatedReason.Respawn, "设置角色复活后的硬直时间。"); + if (!_decisionPoints.TryGetValue(character, out DecisionPoints? dp) || dp is null) + { + dp = new(); + _decisionPoints[character] = dp; + } + await OnQueueUpdatedAsync(_queue, character, dp, hardnessTime, QueueUpdatedReason.Respawn, "设置角色复活后的硬直时间。"); } /// @@ -3199,6 +3465,19 @@ namespace Milimoe.FunGame.Core.Model /// public async Task SetCharacterPreCastSuperSkill(Character character, Skill skill) { + if (_decisionPoints.TryGetValue(character, out DecisionPoints? dp) && dp != null) + { + if (dp.CurrentDecisionPoints < 3) + { + WriteLine("[ " + character + " ] 决策点不足,无法预释放爆发技。"); + return; + } + } + else + { + dp = new(); + _decisionPoints[character] = dp; + } if (character.CharacterState == CharacterState.Casting) { _castingSkills.Remove(character); @@ -3244,7 +3523,7 @@ namespace Milimoe.FunGame.Core.Model AddCharacter(character, newHardnessTime, false); skill.OnSkillCasting(this, character, []); - await OnQueueUpdatedAsync(_queue, character, 0, QueueUpdatedReason.PreCastSuperSkill, "设置角色预释放爆发技的硬直时间。"); + await OnQueueUpdatedAsync(_queue, character, dp, 0, QueueUpdatedReason.PreCastSuperSkill, "设置角色预释放爆发技的硬直时间。"); } } @@ -3513,7 +3792,7 @@ namespace Milimoe.FunGame.Core.Model #region 事件 - public delegate Task TurnStartEventHandler(GamingQueue queue, Character character, List enemys, List teammates, List skills, List items); + public delegate Task TurnStartEventHandler(GamingQueue queue, Character character, DecisionPoints dp, List enemys, List teammates, List skills, List items); /// /// 回合开始事件 /// @@ -3522,17 +3801,18 @@ namespace Milimoe.FunGame.Core.Model /// 回合开始事件 /// /// + /// /// /// /// /// /// - protected async Task OnTurnStartAsync(Character character, List enemys, List teammates, List skills, List items) + protected async Task OnTurnStartAsync(Character character, DecisionPoints dp, List enemys, List teammates, List skills, List items) { - return await (TurnStart?.Invoke(this, character, enemys, teammates, skills, items) ?? Task.FromResult(true)); + return await (TurnStart?.Invoke(this, character, dp, enemys, teammates, skills, items) ?? Task.FromResult(true)); } - public delegate Task TurnEndEventHandler(GamingQueue queue, Character character); + public delegate Task TurnEndEventHandler(GamingQueue queue, Character character, DecisionPoints dp); /// /// 回合结束事件 /// @@ -3541,13 +3821,14 @@ namespace Milimoe.FunGame.Core.Model /// 回合结束事件 /// /// + /// /// - protected async Task OnTurnEndAsync(Character character) + protected async Task OnTurnEndAsync(Character character, DecisionPoints dp) { - await (TurnEnd?.Invoke(this, character) ?? Task.CompletedTask); + await (TurnEnd?.Invoke(this, character, dp) ?? Task.CompletedTask); } - public delegate Task DecideActionEventHandler(GamingQueue queue, Character character, List enemys, List teammates, List skills, List items); + public delegate Task DecideActionEventHandler(GamingQueue queue, Character character, DecisionPoints dp, List enemys, List teammates, List skills, List items); /// /// 决定角色的行动事件 /// @@ -3556,14 +3837,15 @@ namespace Milimoe.FunGame.Core.Model /// 决定角色的行动事件 /// /// + /// /// /// /// /// /// - protected async Task OnDecideActionAsync(Character character, List enemys, List teammates, List skills, List items) + protected async Task OnDecideActionAsync(Character character, DecisionPoints dp, List enemys, List teammates, List skills, List items) { - return await (DecideAction?.Invoke(this, character, enemys, teammates, skills, items) ?? Task.FromResult(CharacterActionType.None)); + return await (DecideAction?.Invoke(this, character, dp, enemys, teammates, skills, items) ?? Task.FromResult(CharacterActionType.None)); } public delegate Task SelectSkillEventHandler(GamingQueue queue, Character character, List skills); @@ -3760,7 +4042,7 @@ namespace Milimoe.FunGame.Core.Model await (DamageToEnemy?.Invoke(this, actor, enemy, damage, actualDamage, isNormalAttack, damageType, magicType, damageResult) ?? Task.CompletedTask); } - public delegate Task CharacterNormalAttackEventHandler(GamingQueue queue, Character actor, List targets); + public delegate Task CharacterNormalAttackEventHandler(GamingQueue queue, Character actor, DecisionPoints dp, List targets); /// /// 角色普通攻击事件 /// @@ -3769,14 +4051,15 @@ namespace Milimoe.FunGame.Core.Model /// 角色普通攻击事件 /// /// + /// /// /// - protected async Task OnCharacterNormalAttackAsync(Character actor, List targets) + protected async Task OnCharacterNormalAttackAsync(Character actor, DecisionPoints dp, List targets) { - await (CharacterNormalAttack?.Invoke(this, actor, targets) ?? Task.CompletedTask); + await (CharacterNormalAttack?.Invoke(this, actor, dp, targets) ?? Task.CompletedTask); } - public delegate Task CharacterPreCastSkillEventHandler(GamingQueue queue, Character actor, SkillTarget skillTarget); + public delegate Task CharacterPreCastSkillEventHandler(GamingQueue queue, Character actor, DecisionPoints dp, SkillTarget skillTarget); /// /// 角色吟唱技能事件(包括直接释放战技) /// @@ -3785,14 +4068,15 @@ namespace Milimoe.FunGame.Core.Model /// 角色吟唱技能事件(包括直接释放战技) /// /// + /// /// /// - protected async Task OnCharacterPreCastSkillAsync(Character actor, SkillTarget skillTarget) + protected async Task OnCharacterPreCastSkillAsync(Character actor, DecisionPoints dp, SkillTarget skillTarget) { - await (CharacterPreCastSkill?.Invoke(this, actor, skillTarget) ?? Task.CompletedTask); + await (CharacterPreCastSkill?.Invoke(this, actor, dp, skillTarget) ?? Task.CompletedTask); } - public delegate Task CharacterCastSkillEventHandler(GamingQueue queue, Character actor, SkillTarget skillTarget, double cost); + public delegate Task CharacterCastSkillEventHandler(GamingQueue queue, Character actor, DecisionPoints dp, SkillTarget skillTarget, double cost); /// /// 角色释放技能事件 /// @@ -3801,15 +4085,16 @@ namespace Milimoe.FunGame.Core.Model /// 角色释放技能事件 /// /// + /// /// /// /// - protected async Task OnCharacterCastSkillAsync(Character actor, SkillTarget skillTarget, double cost) + protected async Task OnCharacterCastSkillAsync(Character actor, DecisionPoints dp, SkillTarget skillTarget, double cost) { - await (CharacterCastSkill?.Invoke(this, actor, skillTarget, cost) ?? Task.CompletedTask); + await (CharacterCastSkill?.Invoke(this, actor, dp, skillTarget, cost) ?? Task.CompletedTask); } - public delegate Task CharacterUseItemEventHandler(GamingQueue queue, Character actor, Item item, List targets); + public delegate Task CharacterUseItemEventHandler(GamingQueue queue, Character actor, DecisionPoints dp, Item item, List targets); /// /// 角色使用物品事件 /// @@ -3818,15 +4103,16 @@ namespace Milimoe.FunGame.Core.Model /// 角色使用物品事件 /// /// + /// /// /// /// - protected async Task OnCharacterUseItemAsync(Character actor, Item item, List targets) + protected async Task OnCharacterUseItemAsync(Character actor, DecisionPoints dp, Item item, List targets) { - await (CharacterUseItem?.Invoke(this, actor, item, targets) ?? Task.CompletedTask); + await (CharacterUseItem?.Invoke(this, actor, dp, item, targets) ?? Task.CompletedTask); } - public delegate Task CharacterCastItemSkillEventHandler(GamingQueue queue, Character actor, Item item, SkillTarget skillTarget, double costMP, double costEP); + public delegate Task CharacterCastItemSkillEventHandler(GamingQueue queue, Character actor, DecisionPoints dp, Item item, SkillTarget skillTarget, double costMP, double costEP); /// /// 角色释放物品的技能事件 /// @@ -3835,14 +4121,15 @@ namespace Milimoe.FunGame.Core.Model /// 角色释放物品的技能事件 /// /// + /// /// /// /// /// /// - protected async Task OnCharacterCastItemSkillAsync(Character actor, Item item, SkillTarget skillTarget, double costMP, double costEP) + protected async Task OnCharacterCastItemSkillAsync(Character actor, DecisionPoints dp, Item item, SkillTarget skillTarget, double costMP, double costEP) { - await (CharacterCastItemSkill?.Invoke(this, actor, item, skillTarget, costMP, costEP) ?? Task.CompletedTask); + await (CharacterCastItemSkill?.Invoke(this, actor, dp, item, skillTarget, costMP, costEP) ?? Task.CompletedTask); } public delegate Task CharacterImmunedEventHandler(GamingQueue queue, Character character, Character immune, ISkill skill, Item? item = null); @@ -3863,7 +4150,7 @@ namespace Milimoe.FunGame.Core.Model await (CharacterImmuned?.Invoke(this, character, immune, skill, item) ?? Task.CompletedTask); } - public delegate Task CharacterDoNothingEventHandler(GamingQueue queue, Character actor); + public delegate Task CharacterDoNothingEventHandler(GamingQueue queue, Character actor, DecisionPoints dp); /// /// 角色主动结束回合事件(区别于放弃行动,这个是主动的) /// @@ -3872,13 +4159,14 @@ namespace Milimoe.FunGame.Core.Model /// 角色主动结束回合事件(区别于放弃行动,这个是主动的) /// /// + /// /// - protected async Task OnCharacterDoNothingAsync(Character actor) + protected async Task OnCharacterDoNothingAsync(Character actor, DecisionPoints dp) { - await (CharacterDoNothing?.Invoke(this, actor) ?? Task.CompletedTask); + await (CharacterDoNothing?.Invoke(this, actor, dp) ?? Task.CompletedTask); } - public delegate Task CharacterGiveUpEventHandler(GamingQueue queue, Character actor); + public delegate Task CharacterGiveUpEventHandler(GamingQueue queue, Character actor, DecisionPoints dp); /// /// 角色放弃行动事件 /// @@ -3887,13 +4175,14 @@ namespace Milimoe.FunGame.Core.Model /// 角色放弃行动事件 /// /// + /// /// - protected async Task OnCharacterGiveUpAsync(Character actor) + protected async Task OnCharacterGiveUpAsync(Character actor, DecisionPoints dp) { - await (CharacterGiveUp?.Invoke(this, actor) ?? Task.CompletedTask); + await (CharacterGiveUp?.Invoke(this, actor, dp) ?? Task.CompletedTask); } - public delegate Task CharacterMoveEventHandler(GamingQueue queue, Character actor, Grid grid); + public delegate Task CharacterMoveEventHandler(GamingQueue queue, Character actor, DecisionPoints dp, Grid grid); /// /// 角色移动事件 /// @@ -3902,11 +4191,12 @@ namespace Milimoe.FunGame.Core.Model /// 角色移动事件 /// /// + /// /// /// - protected async Task OnCharacterMoveAsync(Character actor, Grid grid) + protected async Task OnCharacterMoveAsync(Character actor, DecisionPoints dp, Grid grid) { - await (CharacterMove?.Invoke(this, actor, grid) ?? Task.CompletedTask); + await (CharacterMove?.Invoke(this, actor, dp, grid) ?? Task.CompletedTask); } public delegate Task GameEndEventHandler(GamingQueue queue, Character winner); @@ -3924,7 +4214,7 @@ namespace Milimoe.FunGame.Core.Model return await (GameEnd?.Invoke(this, winner) ?? Task.FromResult(true)); } - public delegate Task QueueUpdatedEventHandler(GamingQueue queue, List characters, Character character, double hardnessTime, QueueUpdatedReason reason, string msg); + public delegate Task QueueUpdatedEventHandler(GamingQueue queue, List characters, Character character, DecisionPoints dp, double hardnessTime, QueueUpdatedReason reason, string msg); /// /// 行动顺序表更新事件 /// @@ -3934,13 +4224,49 @@ namespace Milimoe.FunGame.Core.Model /// /// /// + /// /// /// /// /// - protected async Task OnQueueUpdatedAsync(List characters, Character character, double hardnessTime, QueueUpdatedReason reason, string msg = "") + protected async Task OnQueueUpdatedAsync(List characters, Character character, DecisionPoints dp, double hardnessTime, QueueUpdatedReason reason, string msg = "") { - await (QueueUpdated?.Invoke(this, characters, character, hardnessTime, reason, msg) ?? Task.CompletedTask); + await (QueueUpdated?.Invoke(this, characters, character, dp, hardnessTime, reason, msg) ?? Task.CompletedTask); + } + + public delegate Task CharacterActionTakenEventHandler(GamingQueue queue, Character actor, DecisionPoints dp, CharacterActionType type, RoundRecord record); + /// + /// 角色完成行动事件 + /// + public event CharacterActionTakenEventHandler? CharacterActionTaken; + /// + /// 角色完成行动事件 + /// + /// + /// + /// + /// + /// + protected async Task OnCharacterActionTakenAsync(Character actor, DecisionPoints dp, CharacterActionType type, RoundRecord record) + { + await (CharacterActionTaken?.Invoke(this, actor, dp, type, record) ?? Task.CompletedTask); + } + + public delegate Task CharacterDecisionCompletedEventHandler(GamingQueue queue, Character actor, DecisionPoints dp, RoundRecord record); + /// + /// 角色完成决策事件 + /// + public event CharacterDecisionCompletedEventHandler? CharacterDecisionCompleted; + /// + /// 角色完成决策事件 + /// + /// + /// + /// + /// + protected async Task OnCharacterDecisionCompletedAsync(Character actor, DecisionPoints dp, RoundRecord record) + { + await (CharacterDecisionCompleted?.Invoke(this, actor, dp, record) ?? Task.CompletedTask); } #endregion diff --git a/Model/MixGamingQueue.cs b/Model/MixGamingQueue.cs index 01259c6..294b740 100644 --- a/Model/MixGamingQueue.cs +++ b/Model/MixGamingQueue.cs @@ -35,6 +35,25 @@ namespace Milimoe.FunGame.Core.Model } } + /// + /// 角色行动后,进行死亡竞赛幸存者检定 + /// + /// + /// + /// + protected override async Task AfterCharacterAction(Character character, CharacterActionType type) + { + bool result = await base.AfterCharacterAction(character, type); + if (result) + { + if (MaxRespawnTimes != 0 && MaxScoreToWin > 0 && _stats[character].Kills >= MaxScoreToWin) + { + return false; + } + } + return result; + } + /// /// 游戏结束信息 /// diff --git a/Model/RoundRecord.cs b/Model/RoundRecord.cs index a8ad3c3..b875b52 100644 --- a/Model/RoundRecord.cs +++ b/Model/RoundRecord.cs @@ -8,11 +8,12 @@ namespace Milimoe.FunGame.Core.Entity { public int Round { get; set; } = round; public Character Actor { get; set; } = Factory.GetCharacter(); - public CharacterActionType ActionType { get; set; } = CharacterActionType.None; - public List Targets { get; set; } = []; - public Skill? Skill { get; set; } = null; - public string SkillCost { get; set; } = ""; - public Item? Item { get; set; } = null; + public HashSet ActionTypes { get; } = []; + public Dictionary> Targets { get; } = []; + public Dictionary Skills { get; } = []; + public Dictionary SkillsCost { get; set; } = []; + public Dictionary Items { get; set; } = []; + public Dictionary ItemsCost { get; set; } = []; public bool HasKill { get; set; } = false; public List Assists { get; set; } = []; public Dictionary Damages { get; set; } = []; @@ -31,6 +32,18 @@ namespace Milimoe.FunGame.Core.Entity public List RoundRewards { get; set; } = []; public List OtherMessages { get; set; } = []; + public void AddApplyEffects(Character character, params IEnumerable types) + { + if (ApplyEffects.TryGetValue(character, out List? list) && list != null) + { + list.AddRange(types); + } + else + { + ApplyEffects.TryAdd(character, [.. types]); + } + } + public override string ToString() { StringBuilder builder = new(); @@ -46,47 +59,56 @@ namespace Milimoe.FunGame.Core.Entity builder.AppendLine($"[ {Actor} ] 发动了技能:{string.Join(",", Effects.Where(kv => kv.Key == Actor).Select(e => e.Value.Name))}"); } - if (ActionType == CharacterActionType.NormalAttack || ActionType == CharacterActionType.CastSkill || ActionType == CharacterActionType.CastSuperSkill) + foreach (CharacterActionType type in ActionTypes) { - if (ActionType == CharacterActionType.NormalAttack) + if (type == CharacterActionType.PreCastSkill) + { + continue; + } + + if (!Targets.TryGetValue(type, out List? targets) || targets is null) + { + targets = []; + } + + if (type == CharacterActionType.NormalAttack) { builder.Append($"[ {Actor} ] {Actor.NormalAttack.Name} -> "); } - else if (ActionType == CharacterActionType.CastSkill || ActionType == CharacterActionType.CastSuperSkill) + else if (type == CharacterActionType.CastSkill || type == CharacterActionType.CastSuperSkill) { - if (Skill != null) + if (Skills.TryGetValue(type, out Skill? skill) && skill != null) { - builder.Append($"[ {Actor} ] {Skill.Name}({SkillCost})-> "); + string skillCost = SkillsCost.TryGetValue(skill, out string? cost) ? $"({cost})" : ""; + builder.Append($"[ {Actor} ] {skill.Name}{skillCost} -> "); } else { - builder.Append($"释放魔法 -> "); + builder.Append($"技能 -> "); } } - builder.AppendLine(string.Join(" / ", GetTargetsState())); - if (DeathContinuousKilling.Count > 0) builder.AppendLine($"{string.Join("\r\n", DeathContinuousKilling)}"); - if (ActorContinuousKilling.Count > 0) builder.AppendLine($"{string.Join("\r\n", ActorContinuousKilling)}"); - if (Assists.Count > 0) builder.AppendLine($"本回合助攻:[ {string.Join(" ] / [ ", Assists)} ]"); + else if (type == CharacterActionType.UseItem) + { + if (Items.TryGetValue(type, out Item? item) && item != null) + { + string itemCost = ItemsCost.TryGetValue(item, out string? cost) ? $"({cost})" : ""; + builder.Append($"[ {Actor} ] {item.Name}{itemCost} -> "); + } + else + { + builder.Append($"技能 -> "); + } + } + builder.AppendLine(string.Join(" / ", GetTargetsState(type, targets))); } - if (ActionType == CharacterActionType.PreCastSkill && Skill != null) + if (DeathContinuousKilling.Count > 0) builder.AppendLine($"{string.Join("\r\n", DeathContinuousKilling)}"); + if (ActorContinuousKilling.Count > 0) builder.AppendLine($"{string.Join("\r\n", ActorContinuousKilling)}"); + if (Assists.Count > 0) builder.AppendLine($"本回合助攻:[ {string.Join(" ] / [ ", Assists)} ]"); + + if (ActionTypes.Any(type => type == CharacterActionType.PreCastSkill) && Skills.TryGetValue(CharacterActionType.PreCastSkill, out Skill? magic) && magic != null) { - if (Skill.IsMagic) - { - builder.AppendLine($"[ {Actor} ] 吟唱 [ {Skill.Name} ],持续时间:{CastTime:0.##}"); - } - else - { - builder.Append($"[ {Actor} ] {Skill.Name}({SkillCost})-> "); - builder.AppendLine(string.Join(" / ", GetTargetsState())); - builder.AppendLine($"[ {Actor} ] 回合结束,硬直时间:{HardnessTime:0.##}"); - } - } - else if (ActionType == CharacterActionType.UseItem && Item != null) - { - builder.Append($"[ {Actor} ] {Item.Name}{(SkillCost != "" ? $"({SkillCost})" : " ")}-> "); - builder.AppendLine(string.Join(" / ", GetTargetsState())); - builder.AppendLine($"[ {Actor} ] 回合结束,硬直时间:{HardnessTime:0.##}"); + builder.AppendLine($"[ {Actor} ] 吟唱 [ {magic.Name} ],持续时间:{CastTime:0.##}"); } else { @@ -106,10 +128,10 @@ namespace Milimoe.FunGame.Core.Entity return builder.ToString(); } - private List GetTargetsState() + private List GetTargetsState(CharacterActionType type, List targets) { List strings = []; - foreach (Character target in Targets.Distinct()) + foreach (Character target in targets.Distinct()) { string hasDamage = ""; string hasHeal = ""; @@ -133,11 +155,11 @@ namespace Milimoe.FunGame.Core.Entity } if (IsEvaded.ContainsKey(target)) { - if (ActionType == CharacterActionType.NormalAttack) + if (type == CharacterActionType.NormalAttack) { hasEvaded = hasDamage == "" ? "完美闪避" : "闪避"; } - else if ((ActionType == CharacterActionType.PreCastSkill || ActionType == CharacterActionType.CastSkill || ActionType == CharacterActionType.CastSuperSkill)) + else if ((type == CharacterActionType.PreCastSkill || type == CharacterActionType.CastSkill || type == CharacterActionType.CastSuperSkill)) { hasEvaded = "技能免疫"; } diff --git a/Model/TeamGamingQueue.cs b/Model/TeamGamingQueue.cs index 17b0acd..a2cbd1a 100644 --- a/Model/TeamGamingQueue.cs +++ b/Model/TeamGamingQueue.cs @@ -90,22 +90,42 @@ namespace Milimoe.FunGame.Core.Model } /// - /// 角色行动后 + /// 当角色完成决策后 /// /// - /// + /// /// - protected override async Task AfterCharacterAction(Character character, CharacterActionType type) + protected override async Task AfterCharacterDecision(Character character, DecisionPoints dp) { // 如果目标都是队友,会考虑非伤害型助攻 Team? team = GetTeam(character); if (team != null) { - SetNotDamageAssistTime(character, LastRound.Targets.Where(team.IsOnThisTeam)); + SetNotDamageAssistTime(character, LastRound.Targets.Values.SelectMany(c => c).Where(team.IsOnThisTeam)); } else await Task.CompletedTask; } + /// + /// 角色行动后,进行死亡竞赛幸存者检定 + /// + /// + /// + /// + protected override async Task AfterCharacterAction(Character character, CharacterActionType type) + { + bool result = await base.AfterCharacterAction(character, type); + if (result) + { + Team? team = GetTeam(character); + if ((!_teams.Keys.Where(str => str != team?.Name).Any()) || (MaxScoreToWin > 0 && (team?.Score ?? 0) >= MaxScoreToWin)) + { + return false; + } + } + return result; + } + /// /// 死亡结算时 /// @@ -148,10 +168,6 @@ namespace Milimoe.FunGame.Core.Model string[] teamActive = [.. Teams.OrderByDescending(kv => kv.Value.Score).Select(kv => { int activeCount = kv.Value.GetActiveCharacters().Count; - if (kv.Value == killTeam) - { - activeCount += 1; - } return kv.Key + ":" + kv.Value.Score + "(剩余存活人数:" + activeCount + ")"; })]; WriteLine($"\r\n=== 当前死亡竞赛比分 ===\r\n{string.Join("\r\n", teamActive)}");