新增饭补队友和自杀;区分 AI 控制

This commit is contained in:
milimoe 2025-06-23 23:24:01 +08:00
parent 7f2b6466e2
commit 88d6d9a8c9
Signed by: milimoe
GPG Key ID: 9554D37E4B8991D0
3 changed files with 305 additions and 113 deletions

View File

@ -259,6 +259,38 @@ namespace Milimoe.FunGame.Core.Entity
return 0; return 0;
} }
/// <summary>
/// 在应用真实伤害前修改伤害 [ 允许取消伤害 ]
/// </summary>
/// <param name="character"></param>
/// <param name="enemy"></param>
/// <param name="damage"></param>
/// <param name="isNormalAttack"></param>
/// <param name="damageResult"></param>
/// <returns>返回 true 取消伤害</returns>
public virtual bool BeforeApplyTrueDamage(Character character, Character enemy, double damage, bool isNormalAttack, DamageResult damageResult)
{
return false;
}
/// <summary>
/// 伤害应用时触发
/// </summary>
/// <param name="character"></param>
/// <param name="enemy"></param>
/// <param name="damage"></param>
/// <param name="actualDamage"></param>
/// <param name="isNormalAttack"></param>
/// <param name="damageType"></param>
/// <param name="magicType"></param>
/// <param name="damageResult"></param>
/// <param name="shieldMessage"></param>
/// <param name="originalMessage"></param>
public virtual void OnApplyDamage(Character character, Character enemy, double damage, double actualDamage, bool isNormalAttack, DamageType damageType, MagicType magicType, DamageResult damageResult, string shieldMessage, ref string originalMessage)
{
}
/// <summary> /// <summary>
/// 在完成普通攻击动作之后修改硬直时间 /// 在完成普通攻击动作之后修改硬直时间
/// </summary> /// </summary>
@ -989,6 +1021,17 @@ namespace Milimoe.FunGame.Core.Entity
GamingQueue?.ChangeCharacterHardnessTime(character, addValue, isPercentage, isCheckProtected); GamingQueue?.ChangeCharacterHardnessTime(character, addValue, isPercentage, isCheckProtected);
} }
/// <summary>
/// 设置角色为 AI 控制 [ 系统控制 ]
/// </summary>
/// <param name="cancel"></param>
/// <param name="characters"></param>
/// <returns></returns>
public void SetCharactersToAIControl(bool cancel = false, params IEnumerable<Character> characters)
{
GamingQueue?.SetCharactersToAIControl(true, cancel, characters);
}
/// <summary> /// <summary>
/// 检查角色是否在 AI 控制状态 /// 检查角色是否在 AI 控制状态
/// </summary> /// </summary>

View File

@ -184,6 +184,12 @@ namespace Milimoe.FunGame.Core.Interface.Base
/// <returns></returns> /// <returns></returns>
public Dictionary<Character, bool> GetIsTeammateDictionary(Character character, IEnumerable<Character> targets); public Dictionary<Character, bool> GetIsTeammateDictionary(Character character, IEnumerable<Character> targets);
/// <summary>
/// 设置角色为 AI 控制
/// </summary>
/// <returns></returns>
public void SetCharactersToAIControl(bool bySystem = true, bool cancel = false, params IEnumerable<Character> characters);
/// <summary> /// <summary>
/// 检查角色是否在 AI 控制状态 /// 检查角色是否在 AI 控制状态
/// </summary> /// </summary>

View File

@ -52,7 +52,7 @@ namespace Milimoe.FunGame.Core.Model
/// <summary> /// <summary>
/// 角色是否在 AI 控制下 /// 角色是否在 AI 控制下
/// </summary> /// </summary>
public HashSet<Character> CharactersInAI => _charactersInAI; public List<Character> CharactersInAI => [.. _charactersInAIBySystem.Union(_charactersInAIByUser).Distinct()];
/// <summary> /// <summary>
/// 角色数据 /// 角色数据
@ -142,9 +142,14 @@ namespace Milimoe.FunGame.Core.Model
protected readonly List<Character> _eliminated = []; protected readonly List<Character> _eliminated = [];
/// <summary> /// <summary>
/// 角色是否在 AI 控制下 /// 角色是否在 AI 控制下 [ 系统控制 ]
/// </summary> /// </summary>
protected readonly HashSet<Character> _charactersInAI = []; protected readonly HashSet<Character> _charactersInAIBySystem = [];
/// <summary>
/// 角色是否在 AI 控制下 [ 玩家手动设置 ]
/// </summary>
protected readonly HashSet<Character> _charactersInAIByUser = [];
/// <summary> /// <summary>
/// 硬直时间表 /// 硬直时间表
@ -492,7 +497,8 @@ namespace Milimoe.FunGame.Core.Model
_continuousKilling.Clear(); _continuousKilling.Clear();
_earnedMoney.Clear(); _earnedMoney.Clear();
_eliminated.Clear(); _eliminated.Clear();
_charactersInAI.Clear(); _charactersInAIBySystem.Clear();
_charactersInAIByUser.Clear();
} }
#endregion #endregion
@ -775,13 +781,22 @@ namespace Milimoe.FunGame.Core.Model
// 最大取消次数 // 最大取消次数
int cancelTimes = 3; int cancelTimes = 3;
// 行动开始前,可以修改可选取的角色列表
Dictionary<Character, int> continuousKillingTemp = new(_continuousKilling);
Dictionary<Character, int> earnedMoneyTemp = new(_earnedMoney);
effects = [.. character.Effects.Where(e => e.IsInEffect)];
foreach (Effect effect in effects)
{
effect.AlterSelectListBeforeAction(character, enemys, teammates, skills, continuousKillingTemp, earnedMoneyTemp);
}
// 作出了什么行动 // 作出了什么行动
CharacterActionType type = CharacterActionType.None; CharacterActionType type = CharacterActionType.None;
// 循环条件: // 循环条件:
// AI 控制下未决策、取消次数大于0 // AI 控制下未决策、取消次数大于0
// 手动控制下:未决策 // 手动控制下:未决策
bool isAI = _charactersInAI.Contains(character); bool isAI = CharactersInAI.Contains(character);
while (!decided && (!isAI || cancelTimes > 0)) while (!decided && (!isAI || cancelTimes > 0))
{ {
type = CharacterActionType.None; type = CharacterActionType.None;
@ -921,14 +936,6 @@ namespace Milimoe.FunGame.Core.Model
} }
} }
Dictionary<Character, int> continuousKillingTemp = new(_continuousKilling);
Dictionary<Character, int> earnedMoneyTemp = new(_earnedMoney);
effects = [.. character.Effects.Where(e => e.IsInEffect)];
foreach (Effect effect in effects)
{
effect.AlterSelectListBeforeAction(character, enemys, teammates, skills, continuousKillingTemp, earnedMoneyTemp);
}
if (type == CharacterActionType.NormalAttack) if (type == CharacterActionType.NormalAttack)
{ {
if (character.CharacterState == CharacterState.NotActionable || if (character.CharacterState == CharacterState.NotActionable ||
@ -972,7 +979,7 @@ namespace Milimoe.FunGame.Core.Model
{ {
// 预使用技能,即开始吟唱逻辑 // 预使用技能,即开始吟唱逻辑
Skill? skill = await OnSelectSkillAsync(character, skills); Skill? skill = await OnSelectSkillAsync(character, skills);
if (skill is null && _charactersInAI.Contains(character) && skills.Count > 0) if (skill is null && CharactersInAI.Contains(character) && skills.Count > 0)
{ {
skill = skills[Random.Shared.Next(skills.Count)]; skill = skills[Random.Shared.Next(skills.Count)];
} }
@ -1154,7 +1161,7 @@ namespace Milimoe.FunGame.Core.Model
{ {
// 使用物品逻辑 // 使用物品逻辑
Item? item = await OnSelectItemAsync(character, items); Item? item = await OnSelectItemAsync(character, items);
if (item is null && _charactersInAI.Contains(character) && items.Count > 0) if (item is null && CharactersInAI.Contains(character) && items.Count > 0)
{ {
// AI 控制下随机选取一个物品 // AI 控制下随机选取一个物品
item = items[Random.Shared.Next(items.Count)]; item = items[Random.Shared.Next(items.Count)];
@ -1356,6 +1363,18 @@ namespace Milimoe.FunGame.Core.Model
} }
} }
/// <summary>
/// 获取复活时间
/// </summary>
/// <param name="character"></param>
/// <param name="times"></param>
/// <returns></returns>
protected virtual double GetRespawnTime(Character character, int times)
{
_continuousKilling.TryGetValue(character, out int coefficient);
return Calculation.Round2Digits(Math.Min(30, character.Level * 0.15 + times * 0.87 + coefficient));
}
/// <summary> /// <summary>
/// 回合开始前触发 /// 回合开始前触发
/// </summary> /// </summary>
@ -1423,6 +1442,17 @@ namespace Milimoe.FunGame.Core.Model
} }
damage += totalDamageBonus.Sum(kv => kv.Value); damage += totalDamageBonus.Sum(kv => kv.Value);
} }
else
{
effects = [.. characters.SelectMany(c => c.Effects.Where(e => e.IsInEffect)).Distinct()];
foreach (Effect effect in effects)
{
if (effect.BeforeApplyTrueDamage(actor, enemy, damage, isNormalAttack, damageResult))
{
damageResult = DamageResult.Evaded;
}
}
}
double actualDamage = damage; double actualDamage = damage;
// 闪避了就没伤害了 // 闪避了就没伤害了
@ -1651,7 +1681,13 @@ namespace Milimoe.FunGame.Core.Model
} }
enemy.HP -= actualDamage; enemy.HP -= actualDamage;
WriteLine($"[ {enemy} ] 受到了 {actualDamage:0.##} 点{damageTypeString}{shieldMsg}"); string strDamageMessage = $"[ {enemy} ] 受到了 {actualDamage:0.##} 点{damageTypeString}{shieldMsg}";
effects = [.. characters.SelectMany(c => c.Effects.Where(e => e.IsInEffect)).Distinct()];
foreach (Effect effect in effects)
{
effect.OnApplyDamage(enemy, actor, damage, actualDamage, isNormalAttack, damageType, magicType, damageResult, shieldMsg, ref strDamageMessage);
}
WriteLine(strDamageMessage);
// 生命偷取,攻击者为全额 // 生命偷取,攻击者为全额
double steal = damage * actor.Lifesteal; double steal = damage * actor.Lifesteal;
@ -1713,89 +1749,6 @@ namespace Milimoe.FunGame.Core.Model
} }
} }
/// <summary>
/// 治疗一个目标
/// </summary>
/// <param name="actor"></param>
/// <param name="target"></param>
/// <param name="heal"></param>
/// <param name="canRespawn"></param>
public async Task HealToTargetAsync(Character actor, Character target, double heal, bool canRespawn = false)
{
if (target.HP == target.MaxHP)
{
return;
}
bool isDead = target.HP <= 0;
Dictionary<Effect, double> totalHealBonus = [];
List<Effect> effects = [.. actor.Effects.Union(target.Effects).Distinct().Where(e => e.IsInEffect)];
foreach (Effect effect in effects)
{
bool changeCanRespawn = false;
double healBonus = effect.AlterHealValueBeforeHealToTarget(actor, target, heal, ref changeCanRespawn, totalHealBonus);
if (changeCanRespawn && !canRespawn)
{
canRespawn = true;
}
}
heal += totalHealBonus.Sum(kv => kv.Value);
if (heal <= 0)
{
return;
}
double realHeal = heal;
if (target.HP > 0 || (isDead && canRespawn))
{
// 用于数据统计,不能是全额,溢出的部分需要扣除
if (target.HP + heal > target.MaxHP)
{
realHeal = target.MaxHP - target.HP;
}
target.HP += heal;
if (!LastRound.Heals.TryAdd(target, heal))
{
LastRound.Heals[target] += heal;
}
}
bool isRespawn = isDead && canRespawn;
if (isRespawn)
{
if (target != actor)
{
WriteLine($"[ {target} ] 被 [ {actor} ] 复苏了,并回复了 {heal:0.##} 点生命值!!");
}
else
{
WriteLine($"[ {target} ] 复苏了,并回复了 {heal:0.##} 点生命值!!");
}
double hp = target.HP;
double mp = target.MP;
await SetCharacterRespawn(target);
target.HP = hp;
target.MP = mp;
}
else
{
WriteLine($"[ {target} ] 回复了 {heal:0.##} 点生命值!");
}
// 添加助攻
SetNotDamageAssistTime(actor, target);
// 统计数据
if (_stats.TryGetValue(actor, out CharacterStatistics? stats) && stats != null)
{
stats.TotalHeal += realHeal;
}
await OnHealToTargetAsync(actor, target, heal, isRespawn);
}
/// <summary> /// <summary>
/// 死亡结算 /// 死亡结算
/// </summary> /// </summary>
@ -1803,11 +1756,25 @@ namespace Milimoe.FunGame.Core.Model
/// <param name="death"></param> /// <param name="death"></param>
public async Task DeathCalculationAsync(Character killer, Character death) public async Task DeathCalculationAsync(Character killer, Character death)
{ {
if (IsTeammate(killer, death))
{
await DeathCalculationByTeammateAsync(killer, death);
return;
}
if (!await OnDeathCalculationAsync(killer, death)) if (!await OnDeathCalculationAsync(killer, death))
{ {
return; return;
} }
if (killer == death)
{
_stats[death].Deaths += 1;
WriteLine($"[ {death} ] 自杀了!");
await DealWithCharacterDied(killer, death);
return;
}
if (!_continuousKilling.TryAdd(killer, 1)) _continuousKilling[killer] += 1; if (!_continuousKilling.TryAdd(killer, 1)) _continuousKilling[killer] += 1;
if (!_maxContinuousKilling.TryAdd(killer, 1) && _continuousKilling[killer] > _maxContinuousKilling[killer]) if (!_maxContinuousKilling.TryAdd(killer, 1) && _continuousKilling[killer] > _maxContinuousKilling[killer])
{ {
@ -1955,6 +1922,125 @@ namespace Milimoe.FunGame.Core.Model
WriteLine(actorContinuousKilling); WriteLine(actorContinuousKilling);
} }
await DealWithCharacterDied(killer, death);
}
/// <summary>
/// 死亡结算,击杀队友的情况
/// </summary>
/// <param name="killer"></param>
/// <param name="death"></param>
/// <returns></returns>
public async Task DeathCalculationByTeammateAsync(Character killer, Character death)
{
if (!await OnDeathCalculationByTeammateAsync(killer, death))
{
return;
}
_stats[death].Deaths += 1;
string msg = $"[ {killer} ] 反补了 [ {death} ]";
LastRound.DeathContinuousKilling.Add(msg);
WriteLine(msg);
await DealWithCharacterDied(killer, death);
}
/// <summary>
/// 治疗一个目标
/// </summary>
/// <param name="actor"></param>
/// <param name="target"></param>
/// <param name="heal"></param>
/// <param name="canRespawn"></param>
public async Task HealToTargetAsync(Character actor, Character target, double heal, bool canRespawn = false)
{
if (target.HP == target.MaxHP)
{
return;
}
bool isDead = target.HP <= 0;
Dictionary<Effect, double> totalHealBonus = [];
List<Effect> effects = [.. actor.Effects.Union(target.Effects).Distinct().Where(e => e.IsInEffect)];
foreach (Effect effect in effects)
{
bool changeCanRespawn = false;
double healBonus = effect.AlterHealValueBeforeHealToTarget(actor, target, heal, ref changeCanRespawn, totalHealBonus);
if (changeCanRespawn && !canRespawn)
{
canRespawn = true;
}
}
heal += totalHealBonus.Sum(kv => kv.Value);
if (heal <= 0)
{
return;
}
double realHeal = heal;
if (target.HP > 0 || (isDead && canRespawn))
{
// 用于数据统计,不能是全额,溢出的部分需要扣除
if (target.HP + heal > target.MaxHP)
{
realHeal = target.MaxHP - target.HP;
}
target.HP += heal;
if (!LastRound.Heals.TryAdd(target, heal))
{
LastRound.Heals[target] += heal;
}
}
bool isRespawn = isDead && canRespawn;
if (isRespawn)
{
if (target != actor)
{
WriteLine($"[ {target} ] 被 [ {actor} ] 复苏了,并回复了 {heal:0.##} 点生命值!!");
}
else
{
WriteLine($"[ {target} ] 复苏了,并回复了 {heal:0.##} 点生命值!!");
}
double hp = target.HP;
double mp = target.MP;
await SetCharacterRespawn(target);
target.HP = hp;
target.MP = mp;
}
else
{
WriteLine($"[ {target} ] 回复了 {heal:0.##} 点生命值!");
}
// 添加助攻
SetNotDamageAssistTime(actor, target);
// 统计数据
if (_stats.TryGetValue(actor, out CharacterStatistics? stats) && stats != null)
{
stats.TotalHeal += realHeal;
}
await OnHealToTargetAsync(actor, target, heal, isRespawn);
}
#endregion
#region -
/// <summary>
/// 需要处理复活和解除施法等
/// </summary>
/// <param name="killer"></param>
/// <param name="death"></param>
/// <returns></returns>
public async Task DealWithCharacterDied(Character killer, Character death)
{
await OnDeathCalculation(death, killer); await OnDeathCalculation(death, killer);
death.EP = 0; death.EP = 0;
@ -1979,7 +2065,7 @@ namespace Milimoe.FunGame.Core.Model
else else
{ {
// 进入复活倒计时 // 进入复活倒计时
double respawnTime = Calculation.Round2Digits(Math.Min(30, death.Level * 0.15 + times * 0.87 + coefficient)); double respawnTime = GetRespawnTime(death, times);
_respawnCountdown.TryAdd(death, respawnTime); _respawnCountdown.TryAdd(death, respawnTime);
LastRound.RespawnCountdowns.TryAdd(death, respawnTime); LastRound.RespawnCountdowns.TryAdd(death, respawnTime);
WriteLine($"[ {death} ] 进入复活倒计时:{respawnTime:0.##} {GameplayEquilibriumConstant.InGameTime}"); WriteLine($"[ {death} ] 进入复活倒计时:{respawnTime:0.##} {GameplayEquilibriumConstant.InGameTime}");
@ -2010,10 +2096,6 @@ namespace Milimoe.FunGame.Core.Model
} }
} }
#endregion
#region -
/// <summary> /// <summary>
/// 使用物品实际逻辑 /// 使用物品实际逻辑
/// </summary> /// </summary>
@ -2145,7 +2227,7 @@ namespace Milimoe.FunGame.Core.Model
effect.AlterSelectListBeforeSelection(caster, skill, enemys, teammates); effect.AlterSelectListBeforeSelection(caster, skill, enemys, teammates);
} }
List<Character> targets = await OnSelectSkillTargetsAsync(caster, skill, enemys, teammates); List<Character> targets = await OnSelectSkillTargetsAsync(caster, skill, enemys, teammates);
if (targets.Count == 0 && _charactersInAI.Contains(caster)) if (targets.Count == 0 && CharactersInAI.Contains(caster))
{ {
targets = skill.SelectTargets(caster, enemys, teammates); targets = skill.SelectTargets(caster, enemys, teammates);
} }
@ -2168,7 +2250,7 @@ namespace Milimoe.FunGame.Core.Model
effect.AlterSelectListBeforeSelection(character, attack, enemys, teammates); effect.AlterSelectListBeforeSelection(character, attack, enemys, teammates);
} }
List<Character> targets = await OnSelectNormalAttackTargetsAsync(character, attack, enemys, teammates); List<Character> targets = await OnSelectNormalAttackTargetsAsync(character, attack, enemys, teammates);
if (targets.Count == 0 && _charactersInAI.Contains(character)) if (targets.Count == 0 && CharactersInAI.Contains(character))
{ {
targets = character.NormalAttack.GetSelectableTargets(character, enemys, teammates); targets = character.NormalAttack.GetSelectableTargets(character, enemys, teammates);
if (targets.Count > 0) if (targets.Count > 0)
@ -2642,7 +2724,7 @@ namespace Milimoe.FunGame.Core.Model
protected async Task WillPreCastSuperSkill() protected async Task WillPreCastSuperSkill()
{ {
// 选取所有 AI 控制角色 // 选取所有 AI 控制角色
foreach (Character other in _queue.Where(c => c.CharacterState == CharacterState.Actionable && _charactersInAI.Contains(c)).ToList()) foreach (Character other in _queue.Where(c => c.CharacterState == CharacterState.Actionable && CharactersInAI.Contains(c)).ToList())
{ {
// 有 65% 欲望插队 // 有 65% 欲望插队
if (Random.Shared.NextDouble() < 0.65) if (Random.Shared.NextDouble() < 0.65)
@ -2810,21 +2892,46 @@ namespace Milimoe.FunGame.Core.Model
/// <summary> /// <summary>
/// 设置角色为 AI 控制 /// 设置角色为 AI 控制
/// </summary> /// </summary>
/// <param name="bySystem"></param>
/// <param name="cancel"></param> /// <param name="cancel"></param>
/// <param name="characters"></param> /// <param name="characters"></param>
public void SetCharactersToAIControl(bool cancel = false, params IEnumerable<Character> characters) public void SetCharactersToAIControl(bool bySystem = true, bool cancel = false, params IEnumerable<Character> characters)
{ {
foreach (Character character in characters) foreach (Character character in characters)
{ {
if (cancel) if (cancel)
{ {
_charactersInAI.Remove(character); if (bySystem)
{
_charactersInAIBySystem.Remove(character);
} }
else else
{ {
_charactersInAI.Add(character); _charactersInAIByUser.Remove(character);
} }
} }
else
{
if (bySystem)
{
_charactersInAIBySystem.Add(character);
}
else
{
_charactersInAIByUser.Add(character);
}
}
}
}
/// <summary>
/// 设置角色为 AI 控制 [ 玩家手动设置 ]
/// </summary>
/// <param name="cancel"></param>
/// <param name="characters"></param>
public void SetCharactersToAIControl(bool cancel = false, params IEnumerable<Character> characters)
{
SetCharactersToAIControl(false, cancel, characters);
} }
/// <summary> /// <summary>
@ -2834,7 +2941,27 @@ namespace Milimoe.FunGame.Core.Model
/// <returns></returns> /// <returns></returns>
public bool IsCharacterInAIControlling(Character character) public bool IsCharacterInAIControlling(Character character)
{ {
return _charactersInAI.Contains(character); return CharactersInAI.Contains(character);
}
/// <summary>
/// 检查角色是否在 AI 控制状态 [ 系统控制 ]
/// </summary>
/// <param name="character"></param>
/// <returns></returns>
public bool IsCharacterInAIControllingBySystem(Character character)
{
return _charactersInAIBySystem.Contains(character);
}
/// <summary>
/// 检查角色是否在 AI 控制状态 [ 玩家手动设置 ]
/// </summary>
/// <param name="character"></param>
/// <returns></returns>
public bool IsCharacterInAIControllingByUser(Character character)
{
return _charactersInAIByUser.Contains(character);
} }
#endregion #endregion
@ -3143,6 +3270,22 @@ namespace Milimoe.FunGame.Core.Model
return await (DeathCalculation?.Invoke(this, killer, death) ?? Task.FromResult(true)); return await (DeathCalculation?.Invoke(this, killer, death) ?? Task.FromResult(true));
} }
public delegate Task<bool> DeathCalculationByTeammateEventHandler(GamingQueue queue, Character killer, Character death);
/// <summary>
/// 死亡结算(击杀队友)事件
/// </summary>
public event DeathCalculationEventHandler? DeathCalculationByTeammate;
/// <summary>
/// 死亡结算(击杀队友)事件
/// </summary>
/// <param name="killer"></param>
/// <param name="death"></param>
/// <returns></returns>
protected async Task<bool> OnDeathCalculationByTeammateAsync(Character killer, Character death)
{
return await (DeathCalculationByTeammate?.Invoke(this, killer, death) ?? Task.FromResult(true));
}
public delegate Task<bool> CharacterDeathEventHandler(GamingQueue queue, Character current, Character death); public delegate Task<bool> CharacterDeathEventHandler(GamingQueue queue, Character current, Character death);
/// <summary> /// <summary>
/// 角色死亡事件,此事件位于 <see cref="DeathCalculation"/> 之后 /// 角色死亡事件,此事件位于 <see cref="DeathCalculation"/> 之后