决策点实现:回合内多次行动 (#144)

* 添加决策点功能

* 修复回合日志记录错误

* 吟唱和结算未正确显示

* 事件传递决策点参数;修复第二次行动相关列表不更新的问题

* 决策点可定制;添加事件;事件和钩子优化;修复BUG
This commit is contained in:
milimoe 2026-01-01 04:48:10 +08:00 committed by GitHub
parent b9fbed9c68
commit ea22adf9cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1369 additions and 663 deletions

View File

@ -12,19 +12,20 @@ namespace Milimoe.FunGame.Core.Controller
private readonly GameMap _map = map;
/// <summary>
/// AI的核心决策方法根据当前游戏状态为角色选择最佳行动
/// AI的核心决策方法根据当前游戏状态为角色选择最佳行动
/// </summary>
/// <param name="character">当前行动的AI角色。</param>
/// <param name="startGrid">角色的起始格子。</param>
/// <param name="allPossibleMoveGrids">从起始格子可达的所有移动格子(包括起始格子本身)。</param>
/// <param name="availableSkills">角色所有可用的技能已过滤CD和EP/MP。</param>
/// <param name="availableItems">角色所有可用的物品已过滤CD和EP/MP。</param>
/// <param name="allEnemysInGame">场上所有敌人。</param>
/// <param name="allTeammatesInGame">场上所有队友。</param>
/// <param name="selectableEnemys">场上能够选取的敌人。</param>
/// <param name="selectableTeammates">场上能够选取的队友。</param>
/// <returns>包含最佳行动的AIDecision对象。</returns>
public async Task<AIDecision> DecideAIActionAsync(Character character, Grid startGrid, List<Grid> allPossibleMoveGrids,
/// <param name="character">当前行动的AI角色</param>
/// <param name="dp">角色的决策点</param>
/// <param name="startGrid">角色的起始格子</param>
/// <param name="allPossibleMoveGrids">从起始格子可达的所有移动格子(包括起始格子本身)</param>
/// <param name="availableSkills">角色所有可用的技能已过滤CD和EP/MP</param>
/// <param name="availableItems">角色所有可用的物品已过滤CD和EP/MP</param>
/// <param name="allEnemysInGame">场上所有敌人</param>
/// <param name="allTeammatesInGame">场上所有队友</param>
/// <param name="selectableEnemys">场上能够选取的敌人</param>
/// <param name="selectableTeammates">场上能够选取的队友</param>
/// <returns>包含最佳行动的AIDecision对象</returns>
public async Task<AIDecision> DecideAIActionAsync(Character character, DecisionPoints dp, Grid startGrid, List<Grid> allPossibleMoveGrids,
List<Skill> availableSkills, List<Item> availableItems, List<Character> allEnemysInGame, List<Character> allTeammatesInGame,
List<Character> selectableEnemys, List<Character> 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<Grid> 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<Grid> 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;
}

View File

@ -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
/// 局内使用物品触发
/// </summary>
/// <returns></returns>
public async Task<bool> UseItem(IGamingQueue queue, Character character, List<Character> enemys, List<Character> teammates)
public async Task<bool> UseItem(IGamingQueue queue, Character character, DecisionPoints dp, List<Character> enemys, List<Character> 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)
{

View File

@ -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
/// 行动开始前,指定角色的行动,而不是使用顺序表自带的逻辑;或者修改对应的操作触发概率
/// </summary>
/// <param name="character"></param>
/// <param name="dp"></param>
/// <param name="state"></param>
/// <param name="canUseItem"></param>
/// <param name="canCastSkill"></param>
@ -591,7 +593,7 @@ namespace Milimoe.FunGame.Core.Entity
/// <param name="pNormalAttack"></param>
/// <param name="forceAction"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 在角色行动后触发
/// </summary>
/// <param name="actor"></param>
/// <param name="dp"></param>
/// <param name="type"></param>
public virtual void OnCharacterActionTaken(Character actor, DecisionPoints dp, CharacterActionType type)
{
}
/// <summary>
/// 在角色回合决策结束后触发
/// </summary>
/// <param name="actor"></param>
/// <param name="dp"></param>
public virtual void OnCharacterDecisionCompleted(Character actor, DecisionPoints dp)
{
}
/// <summary>
/// 对敌人造成技能伤害 [ 强烈建议使用此方法造成伤害而不是自行调用 <see cref="IGamingQueue.DamageToEnemyAsync"/> ]
/// </summary>
@ -1073,13 +1096,7 @@ namespace Milimoe.FunGame.Core.Entity
/// </summary>
/// <param name="character"></param>
/// <param name="types"></param>
public void RecordCharacterApplyEffects(Character character, params List<EffectType> types)
{
if (GamingQueue?.LastRound.ApplyEffects.TryAdd(character, types) ?? false)
{
GamingQueue?.LastRound.ApplyEffects[character].AddRange(types);
}
}
public void RecordCharacterApplyEffects(Character character, params List<EffectType> types) => GamingQueue?.LastRound.AddApplyEffects(character, types);
/// <summary>
/// 返回特效详情

View File

@ -16,7 +16,7 @@ namespace Milimoe.FunGame.Core.Entity
/// <summary>
/// 普通攻击说明
/// </summary>
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) : "")}。";
/// <summary>
/// 普通攻击的通用说明
@ -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
/// </summary>
private bool _isMagic = isMagic;
/// <summary>
/// 指示普通攻击是否由武器附魔
/// </summary>
private bool _isMagicByWeapon = false;
/// <summary>
/// 魔法类型 [ 生效型 ]
/// </summary>

View File

@ -55,6 +55,11 @@ namespace Milimoe.FunGame.Core.Interface.Base
/// </summary>
public Dictionary<Character, CharacterStatistics> CharacterStatistics { get; }
/// <summary>
/// 角色的决策点
/// </summary>
public Dictionary<Character, DecisionPoints> CharacterDecisionPoints { get; }
/// <summary>
/// 游戏运行的时间
/// </summary>
@ -153,22 +158,24 @@ namespace Milimoe.FunGame.Core.Interface.Base
/// 使用物品
/// </summary>
/// <param name="item"></param>
/// <param name="caster"></param>
/// <param name="character"></param>
/// <param name="dp"></param>
/// <param name="enemys"></param>
/// <param name="teammates"></param>
/// <param name="castRange"></param>
/// <param name="desiredTargets"></param>
/// <returns></returns>
public Task<bool> UseItemAsync(Item item, Character caster, List<Character> enemys, List<Character> teammates, List<Grid> castRange, List<Character>? desiredTargets = null);
public Task<bool> UseItemAsync(Item item, Character character, DecisionPoints dp, List<Character> enemys, List<Character> teammates, List<Grid> castRange, List<Character>? desiredTargets = null);
/// <summary>
/// 角色移动
/// </summary>
/// <param name="character"></param>
/// <param name="dp"></param>
/// <param name="target"></param>
/// <param name="startGrid"></param>
/// <returns></returns>
public Task<bool> CharacterMoveAsync(Character character, Grid target, Grid? startGrid);
public Task<bool> CharacterMoveAsync(Character character, DecisionPoints dp, Grid target, Grid? startGrid);
/// <summary>
/// 选取移动目标

View File

@ -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<Character>(ref reader, options) ?? Factory.GetCharacter();
break;
case nameof(RoundRecord.Targets):
List<Character> targets = NetworkUtility.JsonDeserialize<List<Character>>(ref reader, options) ?? [];
result.Targets.AddRange(targets);
Dictionary<CharacterActionType, List<Character>> targets = NetworkUtility.JsonDeserialize<Dictionary<CharacterActionType, List<Character>>>(ref reader, options) ?? [];
foreach (CharacterActionType type in targets.Keys)
{
result.Targets[type] = targets[type];
}
break;
case nameof(RoundRecord.Damages):
Dictionary<Guid, double> damagesGuid = NetworkUtility.JsonDeserialize<Dictionary<Guid, double>>(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<CharacterActionType> types = NetworkUtility.JsonDeserialize<List<CharacterActionType>>(ref reader, options) ?? [];
foreach (CharacterActionType type in types)
{
result.ActionTypes.Add(type);
}
break;
case nameof(RoundRecord.Skill):
result.Skill = NetworkUtility.JsonDeserialize<Skill>(ref reader, options);
case nameof(RoundRecord.Skills):
Dictionary<CharacterActionType, Skill> skills = NetworkUtility.JsonDeserialize<Dictionary<CharacterActionType, Skill>>(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<Skill, string> skillsCost = NetworkUtility.JsonDeserialize<Dictionary<Skill, string>>(ref reader, options) ?? [];
foreach (Skill skill in skillsCost.Keys)
{
result.SkillsCost[skill] = skillsCost[skill];
}
break;
case nameof(RoundRecord.Item):
result.Item = NetworkUtility.JsonDeserialize<Item>(ref reader, options);
case nameof(RoundRecord.Items):
Dictionary<CharacterActionType, Item> items = NetworkUtility.JsonDeserialize<Dictionary<CharacterActionType, Item>>(ref reader, options) ?? [];
foreach (CharacterActionType type in items.Keys)
{
result.Items[type] = items[type];
}
break;
case nameof(RoundRecord.ItemsCost):
Dictionary<Item, string> itemsCost = NetworkUtility.JsonDeserialize<Dictionary<Item, string>>(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);

169
Model/DecisionPoints.cs Normal file
View File

@ -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
{
/// <summary>
/// 所用的游戏平衡常数
/// </summary>
[JsonIgnore]
public EquilibriumConstant GameplayEquilibriumConstant { get; set; } = General.GameplayEquilibriumConstant;
/// <summary>
/// 当前决策点
/// </summary>
public int CurrentDecisionPoints { get; set; } = General.GameplayEquilibriumConstant.InitialDecisionPoints;
/// <summary>
/// 决策点上限
/// </summary>
public int MaxDecisionPoints { get; set; } = General.GameplayEquilibriumConstant.InitialDecisionPoints;
/// <summary>
/// 每回合决策点补充数量
/// </summary>
public int RecoverDecisionPointsPerRound { get; set; } = General.GameplayEquilibriumConstant.RecoverDecisionPointsPerRound;
/// <summary>
/// 本回合已补充的决策点
/// </summary>
public int DecisionPointsRecovery { get; set; } = 0;
/// <summary>
/// 是否释放过勇气指令
/// </summary>
public bool CourageCommandSkill { get; set; } = false;
/// <summary>
/// 记录本回合已使用的行动类型和次数
/// </summary>
public Dictionary<CharacterActionType, int> ActionTypes { get; } = [];
/// <summary>
/// 记录本回合行动的硬直时间
/// </summary>
public List<double> ActionsHardnessTime { get; } = [];
/// <summary>
/// 本回合已进行的行动次数
/// </summary>
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;
/// <summary>
/// 获取当前决策点配额
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
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,
};
}
}
/// <summary>
/// 添加临时决策点配额 [ 回合结束时清除 ]
/// </summary>
/// <param name="type"></param>
/// <param name="add"></param>
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;
}
}
/// <summary>
/// 清除临时决策点配额
/// </summary>
public void ClearTempActionQuota()
{
_tempActionQuotaNormalAttack = 0;
_tempActionQuotaSuperSkill = 0;
_tempActionQuotaSkill = 0;
_tempActionQuotaItem = 0;
_tempActionQuotaOther = 0;
}
/// <summary>
/// 累计行动类型和次数
/// </summary>
/// <param name="type"></param>
/// <param name="addActionTaken"></param>
public void AddActionType(CharacterActionType type, bool addActionTaken = true)
{
if (addActionTaken) ActionsTaken++;
if (!ActionTypes.TryAdd(type, 1))
{
ActionTypes[type]++;
}
}
/// <summary>
/// 判断行动类型是否达到配额
/// </summary>
/// <param name="type"></param>
public bool CheckActionTypeQuota(CharacterActionType type)
{
if (ActionTypes.TryGetValue(type, out int times))
{
return times < this[type];
}
return true;
}
/// <summary>
/// 获取决策点消耗
/// </summary>
/// <param name="type"></param>
/// <param name="skill"></param>
/// <returns></returns>
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
};
}
}
}

View File

@ -540,6 +540,86 @@ namespace Milimoe.FunGame.Core.Model
/// </summary>
public int RoleMOV_Medic { get; set; } = 3;
/// <summary>
/// 初始决策点数
/// </summary>
public int InitialDecisionPoints { get; set; } = 1;
/// <summary>
/// 决策点上限
/// </summary>
public int MaxDecisionPoints { get; set; } = 7;
/// <summary>
/// 每回合决策点补充数量
/// </summary>
public int RecoverDecisionPointsPerRound { get; set; } = 1;
/// <summary>
/// 每回合普通攻击决策配额
/// </summary>
public int ActionQuotaNormalAttack { get; set; } = 1;
/// <summary>
/// 每回合战技决策配额
/// </summary>
public int ActionQuotaSkill { get; set; } = 1;
/// <summary>
/// 每回合爆发技决策配额
/// </summary>
public int ActionQuotaSuperSkill { get; set; } = 1;
/// <summary>
/// 每回合魔法决策配额 [ 使用魔法因为会进入吟唱态,所以无论是否设置都没意义 ]
/// </summary>
public int ActionQuotaMagic { get; set; } = 1;
/// <summary>
/// 每回合使用物品决策配额
/// </summary>
public int ActionQuotaItem { get; set; } = 1;
/// <summary>
/// 每回合其他决策配额
/// </summary>
public int ActionQuotaOther { get; set; } = 0;
/// <summary>
/// 普通攻击决策点消耗
/// </summary>
public int DecisionPointsCostNormalAttack { get; set; } = 1;
/// <summary>
/// 战技决策点消耗
/// </summary>
public int DecisionPointsCostSkill { get; set; } = 2;
/// <summary>
/// 爆发技决策点消耗(回合内)
/// </summary>
public int DecisionPointsCostSuperSkill { get; set; } = 2;
/// <summary>
/// 爆发技决策点消耗(回合外)
/// </summary>
public int DecisionPointsCostSuperSkillOutOfTurn { get; set; } = 3;
/// <summary>
/// 魔法决策点消耗
/// </summary>
public int DecisionPointsCostMagic { get; set; } = 2;
/// <summary>
/// 使用物品决策点消耗
/// </summary>
public int DecisionPointsCostItem { get; set; } = 1;
/// <summary>
/// 其他决策点消耗
/// </summary>
public int DecisionPointsCostOther { get; set; } = 1;
/// <summary>
/// 应用此游戏平衡常数给实体
/// </summary>

File diff suppressed because it is too large Load Diff

View File

@ -35,6 +35,25 @@ namespace Milimoe.FunGame.Core.Model
}
}
/// <summary>
/// 角色行动后,进行死亡竞赛幸存者检定
/// </summary>
/// <param name="character"></param>
/// <param name="type"></param>
/// <returns></returns>
protected override async Task<bool> 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;
}
/// <summary>
/// 游戏结束信息
/// </summary>

View File

@ -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<Character> Targets { get; set; } = [];
public Skill? Skill { get; set; } = null;
public string SkillCost { get; set; } = "";
public Item? Item { get; set; } = null;
public HashSet<CharacterActionType> ActionTypes { get; } = [];
public Dictionary<CharacterActionType, List<Character>> Targets { get; } = [];
public Dictionary<CharacterActionType, Skill> Skills { get; } = [];
public Dictionary<Skill, string> SkillsCost { get; set; } = [];
public Dictionary<CharacterActionType, Item> Items { get; set; } = [];
public Dictionary<Item, string> ItemsCost { get; set; } = [];
public bool HasKill { get; set; } = false;
public List<Character> Assists { get; set; } = [];
public Dictionary<Character, double> Damages { get; set; } = [];
@ -31,6 +32,18 @@ namespace Milimoe.FunGame.Core.Entity
public List<Skill> RoundRewards { get; set; } = [];
public List<string> OtherMessages { get; set; } = [];
public void AddApplyEffects(Character character, params IEnumerable<EffectType> types)
{
if (ApplyEffects.TryGetValue(character, out List<EffectType>? 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<Character>? 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()));
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 (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 (ActionType == CharacterActionType.PreCastSkill && Skill != null)
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<string> GetTargetsState()
private List<string> GetTargetsState(CharacterActionType type, List<Character> targets)
{
List<string> 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 = "技能免疫";
}

View File

@ -90,22 +90,42 @@ namespace Milimoe.FunGame.Core.Model
}
/// <summary>
/// 角色行动
/// 当角色完成决策
/// </summary>
/// <param name="character"></param>
/// <param name="type"></param>
/// <param name="dp"></param>
/// <returns></returns>
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;
}
/// <summary>
/// 角色行动后,进行死亡竞赛幸存者检定
/// </summary>
/// <param name="character"></param>
/// <param name="type"></param>
/// <returns></returns>
protected override async Task<bool> 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;
}
/// <summary>
/// 死亡结算时
/// </summary>
@ -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)}");