532 lines
21 KiB
C#
532 lines
21 KiB
C#
using System.Collections.Generic;
|
||
using System.Linq;
|
||
using Continentis.MainGame.Card;
|
||
using Continentis.MainGame.Equipment;
|
||
using SLSFramework.General;
|
||
using SLSFramework.UModAssistance;
|
||
using UnityEngine;
|
||
|
||
namespace Continentis.MainGame.Character
|
||
{
|
||
public partial class CharacterBase
|
||
{
|
||
public virtual void InitializeCards()
|
||
{
|
||
string initialPile = this is PlayerHero ? "Draw" : "Pool";
|
||
|
||
foreach (string cardDataID in data.initialDeckRef)
|
||
{
|
||
CardInstance.GenerateCardInstance(ModManager.GetData<CardData>(cardDataID), this, initialPile);
|
||
}
|
||
|
||
foreach (EquipmentBase equipment in equipmentSubmodule.currentEquipments)
|
||
{
|
||
foreach (string cardDataID in equipment.equipmentData.belongingCardDataRefs)
|
||
{
|
||
CardInstance.GenerateCardInstance(ModManager.GetData<CardData>(cardDataID), this, initialPile);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
public partial class CharacterBase
|
||
{
|
||
/// <summary>
|
||
/// 检查是否有足够的体力
|
||
/// </summary>
|
||
public bool CheckEnoughStamina(int staminaCost)
|
||
{
|
||
return GetAttribute("Stamina") >= staminaCost;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 消耗体力
|
||
/// </summary>
|
||
public void ModifyStamina(int staminaValue)
|
||
{
|
||
ModifyAttribute("Stamina", staminaValue);
|
||
ClampAttribute("Stamina", 0, GetAttribute("MaximumStamina"));
|
||
|
||
if (this is PlayerHero)
|
||
{
|
||
CombatUIManager.Instance.combatMainPage.combatResourcesDisplayer.UpdateIcons();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查是否有足够的魔法
|
||
/// </summary>
|
||
public bool CheckEnoughMana(int manaCost)
|
||
{
|
||
return GetAttribute("Mana") >= manaCost;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 消耗魔法
|
||
/// </summary>
|
||
public void ModifyMana(int manaValue)
|
||
{
|
||
ModifyAttribute("Mana", manaValue);
|
||
ClampAttribute("Mana", 0, GetAttribute("MaximumMana"));
|
||
|
||
if (this is PlayerHero)
|
||
{
|
||
CombatUIManager.Instance.combatMainPage.combatResourcesDisplayer.UpdateIcons();
|
||
}
|
||
}
|
||
}
|
||
|
||
public partial class CharacterBase
|
||
{
|
||
/// <summary>
|
||
/// 攻击目标(新版本,使用 AttackContext 携带标签与来源信息)。
|
||
/// </summary>
|
||
/// <param name="target">攻击目标</param>
|
||
/// <param name="startDamage">初始伤害</param>
|
||
/// <param name="context">攻击上下文(包含来源卡牌、标签等);传 null 等价于默认上下文</param>
|
||
/// <returns>攻击结果</returns>
|
||
public AttackResult Attack(CharacterBase target, int startDamage, AttackContext context)
|
||
{
|
||
context ??= new AttackContext();
|
||
|
||
bool isSilent = context.HasTag(AttackTags.Silent);
|
||
bool isHpRemoval = context.HasTag(AttackTags.HpRemoval);
|
||
bool isReactive = context.HasTag(AttackTags.Reactive);
|
||
bool ignoreDodge = context.HasAnyTag(AttackTags.GuaranteedHit, AttackTags.HpRemoval);
|
||
bool ignoreBlock = context.HasAnyTag(AttackTags.Penetrating, AttackTags.HpRemoval);
|
||
bool ignoreShield = context.HasAnyTag(AttackTags.HpRemoval);
|
||
|
||
// 静默和生命移除均不触发 onStartAttack
|
||
if (!isSilent && !isHpRemoval)
|
||
{
|
||
eventSubmodule.onStartAttack.Invoke(target);
|
||
}
|
||
|
||
// 生命移除:直接扣血,跳过所有防御和事件
|
||
if (isHpRemoval)
|
||
{
|
||
target.HealthRemoval(startDamage, context);
|
||
target.characterView.hudContainer.enablingHUDs["MainAttributesBar"].UpdateHud();
|
||
return new AttackResult(this, target, startDamage, context, false, 0, 0, startDamage);
|
||
}
|
||
|
||
// 闪避检测
|
||
int modifiedStartDamageForDodge = Mathf.RoundToInt(startDamage * GetRawAttribute("DodgeCheckStartDamageMultiplier", 1));
|
||
bool dodged = !ignoreDodge && target.CheckDodge(modifiedStartDamageForDodge);
|
||
|
||
int hurt = 0;
|
||
int blocked = 0;
|
||
int shielded = 0;
|
||
|
||
if (!dodged)
|
||
{
|
||
int remainingDamageAfterBlock = ignoreBlock ? startDamage : target.CheckBlock(startDamage);
|
||
if (remainingDamageAfterBlock > 0)
|
||
{
|
||
blocked = startDamage - remainingDamageAfterBlock;
|
||
int remainingDamageAfterShield = ignoreShield ? remainingDamageAfterBlock : target.CheckShield(remainingDamageAfterBlock);
|
||
if (remainingDamageAfterShield > 0)
|
||
{
|
||
shielded = remainingDamageAfterBlock - remainingDamageAfterShield;
|
||
hurt = remainingDamageAfterShield;
|
||
target.HealthRemoval(remainingDamageAfterShield, context);
|
||
}
|
||
}
|
||
}
|
||
|
||
target.characterView.hudContainer.enablingHUDs["MainAttributesBar"].UpdateHud();
|
||
AttackResult attackResult = new AttackResult(this, target, startDamage, context, dodged, blocked, shielded, hurt);
|
||
|
||
if (!isSilent)
|
||
{
|
||
// 角色 EventSubmodule 级别事件(始终触发,用于日志等)
|
||
eventSubmodule.onFinishAttack.Invoke(target, attackResult);
|
||
target.eventSubmodule.onGetAttacked.Invoke(this, attackResult);
|
||
|
||
// Buff 层事件:响应式攻击不触发,防止无限递归
|
||
if (!isReactive)
|
||
{
|
||
combatBuffSubmodule.buffList.For(buff =>
|
||
{
|
||
buff.eventSubmodule?.onDealAttack.Invoke(attackResult);
|
||
});
|
||
|
||
target.combatBuffSubmodule.buffList.For(buff =>
|
||
{
|
||
buff.eventSubmodule?.onGetAttacked.Invoke(attackResult);
|
||
});
|
||
}
|
||
}
|
||
|
||
return attackResult;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 攻击目标(兼容旧版 API,内部转换为 AttackContext 调用)。
|
||
/// 新代码请优先使用 Attack(target, damage, AttackContext) 重载。
|
||
/// </summary>
|
||
public AttackResult Attack(CharacterBase target, int startDamage, CardInstance attackCard = null, bool triggerAttackEvent = true, bool ignoreDodge = false, bool ignoreBlock = false, bool ignoreShield = false)
|
||
{
|
||
var context = new AttackContext(attackCard);
|
||
if (!triggerAttackEvent) context.WithTag(AttackTags.Silent);
|
||
if (ignoreDodge) context.WithTag(AttackTags.GuaranteedHit);
|
||
if (ignoreBlock) context.WithTag(AttackTags.Penetrating);
|
||
return Attack(target, startDamage, context);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查闪避(闪避失败后会清空闪避值)
|
||
/// </summary>
|
||
/// <param name="damage">即将受到的伤害</param>
|
||
/// <returns>是否闪避成功</returns>
|
||
public bool CheckDodge(int damage)
|
||
{
|
||
int dodge = attributeSubmodule.GetGeneralAttribute("Dodge");
|
||
|
||
if (dodge > 0)
|
||
{
|
||
bool success = damage <= dodge;
|
||
|
||
if (success)
|
||
{
|
||
MainGameManager.Instance.basePrefabs.GenerateInfoText("Dodged!", characterView);
|
||
return true;
|
||
}
|
||
else
|
||
{
|
||
attributeSubmodule.generalAttributeGroup.current["Dodge"] = 0;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查格挡(并且扣除格挡值)
|
||
/// </summary>
|
||
/// <param name="damage">即将受到的伤害</param>
|
||
/// <returns>格挡之后的剩余伤害</returns>
|
||
public int CheckBlock(int damage)
|
||
{
|
||
int block = attributeSubmodule.GetGeneralAttribute("Block");
|
||
|
||
if (block > 0)
|
||
{
|
||
bool success = damage <= block;
|
||
int remainingDamage = 0;
|
||
int blockedDamage = block;
|
||
|
||
if (!success)
|
||
{
|
||
attributeSubmodule.generalAttributeGroup.current["Block"] = 0;
|
||
remainingDamage = damage - block;
|
||
}
|
||
else
|
||
{
|
||
attributeSubmodule.generalAttributeGroup.current["Block"] = block - damage;
|
||
blockedDamage = damage;
|
||
}
|
||
|
||
MainGameManager.Instance.basePrefabs.GenerateBlockedText(blockedDamage, characterView);
|
||
return remainingDamage;
|
||
}
|
||
|
||
return damage;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 检查临时生命(并且扣除临时生命值)
|
||
/// </summary>
|
||
/// <param name="damage">即将受到的伤害</param>
|
||
/// <returns>临时生命吸收后的剩余伤害</returns>
|
||
public int CheckShield(int damage)
|
||
{
|
||
int shield = attributeSubmodule.GetGeneralAttribute("TemporaryHealth");
|
||
|
||
if (shield > 0)
|
||
{
|
||
bool success = damage <= shield;
|
||
int remainingDamage = 0;
|
||
int blockedDamage = shield;
|
||
|
||
if (!success)
|
||
{
|
||
attributeSubmodule.generalAttributeGroup.current["TemporaryHealth"] = 0;
|
||
remainingDamage = damage - shield;
|
||
}
|
||
else
|
||
{
|
||
attributeSubmodule.generalAttributeGroup.current["TemporaryHealth"] = shield - damage;
|
||
blockedDamage = damage;
|
||
}
|
||
|
||
MainGameManager.Instance.basePrefabs.GenerateBlockedText(blockedDamage, characterView);
|
||
return remainingDamage;
|
||
}
|
||
|
||
return damage;
|
||
}
|
||
|
||
public void HealthRemoval(int damage, AttackContext context = null)
|
||
{
|
||
int healthBefore = GetAttribute("Health");
|
||
ModifyAttribute("Health", -damage);
|
||
int healthAfter = GetAttribute("Health");
|
||
int maxHealth = GetAttribute("MaximumHealth");
|
||
|
||
Color dmgTextColor = Color.white;
|
||
if (context is { damageKeywords: { Count: > 0 } })
|
||
{
|
||
foreach (string elementTag in MainGameManager.Instance.elementTags)
|
||
{
|
||
if (context.damageKeywords.Contains(elementTag))
|
||
{
|
||
switch (elementTag)
|
||
{
|
||
case "Fire":
|
||
dmgTextColor = Color.red;
|
||
break;
|
||
case "Ice":
|
||
dmgTextColor = Color.cyan;
|
||
break;
|
||
case "Wind":
|
||
dmgTextColor = Color.lightGreen;
|
||
break;
|
||
case "Earth":
|
||
dmgTextColor = Color.darkGoldenRod;
|
||
break;
|
||
case "Storm":
|
||
dmgTextColor = Color.magenta;
|
||
break;
|
||
case "Light":
|
||
dmgTextColor = Color.yellowNice;
|
||
break;
|
||
case "Darkness":
|
||
dmgTextColor = Color.rebeccaPurple;
|
||
break;
|
||
default:
|
||
dmgTextColor = Color.white;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
MainGameManager.Instance.basePrefabs.GenerateHurtText(damage, characterView, dmgTextColor);
|
||
// 血量百分比阈值检查:穿越整十档时通知 LogicBase(如 Boss 阶段切换)
|
||
if (maxHealth > 0 && logicBase != null)
|
||
{
|
||
float percentBefore = (float)healthBefore / maxHealth;
|
||
float percentAfter = (float)healthAfter / maxHealth;
|
||
// 找出所有被穿越的整十档(从高到低依次触发)
|
||
for (int threshold = 9; threshold >= 0; threshold--)
|
||
{
|
||
float t = threshold * 0.1f;
|
||
if (percentBefore > t && percentAfter <= t)
|
||
{
|
||
logicBase.OnHealthThreshold(t);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (GetAttribute("Health") <= 0)
|
||
{
|
||
Die();
|
||
}
|
||
}
|
||
|
||
public void Heal(int heal)
|
||
{
|
||
if (heal <= 0) return;
|
||
|
||
ModifyAttribute("Health", heal);
|
||
ClampAttribute("Health", 0, GetAttribute("MaximumHealth"));
|
||
|
||
MainGameManager.Instance.basePrefabs.GenerateHealText(heal, characterView);
|
||
characterView.hudContainer.UpdateAllHUD();
|
||
}
|
||
}
|
||
|
||
public partial class CharacterBase
|
||
{
|
||
/// <summary>
|
||
/// 添加格挡(格挡每回合结束后会清空)
|
||
/// </summary>
|
||
public void AddBlock(int baseBlock, bool applyOffsetAndModifier = true, CharacterBase target = null)
|
||
{
|
||
target ??= this;
|
||
|
||
if (!applyOffsetAndModifier)
|
||
{
|
||
target.ModifyAttribute("Block", baseBlock);
|
||
}
|
||
else
|
||
{
|
||
int baseBlockAfterOffset = baseBlock + GetAttribute("BlockGainOffset");
|
||
int finalBlock = Mathf.RoundToInt(baseBlockAfterOffset * GetRawAttribute("BlockGainMultiplier", 1));
|
||
target.ModifyAttribute("Block", finalBlock);
|
||
}
|
||
|
||
target.characterView.hudContainer.UpdateAllHUD();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加闪避(闪避在回合结束后或被击中后清空)
|
||
/// </summary>
|
||
public void AddDodge(int dodge, CharacterBase target = null)
|
||
{
|
||
int baseDodgeAfterOffset = dodge + GetAttribute("DodgeGainOffset");
|
||
int finalDodge = Mathf.RoundToInt(baseDodgeAfterOffset * GetRawAttribute("DodgeGainMultiplier", 1));
|
||
|
||
target ??= this;
|
||
target.ModifyAttribute("Dodge", finalDodge);
|
||
target.characterView.hudContainer.UpdateAllHUD();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加临时生命(不会自动清空)
|
||
/// </summary>
|
||
public void AddShield(int shield, CharacterBase target = null)
|
||
{
|
||
int baseShieldAfterOffset = shield + GetAttribute("TemporaryHealthGainOffset");
|
||
int finalShield = Mathf.RoundToInt(baseShieldAfterOffset * GetRawAttribute("TemporaryHealthGainMultiplier", 1));
|
||
|
||
target ??= this;
|
||
target.ModifyAttribute("TemporaryHealth", finalShield);
|
||
target.characterView.hudContainer.UpdateAllHUD();
|
||
}
|
||
}
|
||
|
||
public partial class CharacterBase
|
||
{
|
||
public virtual void RegisterIntention(params IntentionBase[] intentions)
|
||
{
|
||
intentionSubmodule.allIntentions.AddRange(intentions);
|
||
}
|
||
|
||
public virtual void IntentionBrain()
|
||
{
|
||
List<IntentionBase> availableIntentions = intentionSubmodule.allIntentions.Where(intention => intention.Condition()).ToList();
|
||
availableIntentions.Sort();
|
||
intentionSubmodule.currentIntention = availableIntentions.FirstOrDefault() ?? new IntentionBase(intentionSubmodule);
|
||
intentionSubmodule.currentIntention.RefreshCardWeights();
|
||
intentionSubmodule.currentIntention.RefreshTargets();
|
||
}
|
||
|
||
public virtual void GetIntendedCards()
|
||
{
|
||
IntentionBase currentIntention = intentionSubmodule.currentIntention;
|
||
List<CardInstance> availableCards = deckSubmodule.PoolPile;
|
||
List<IntendedCard> intended = new List<IntendedCard>();
|
||
int predictedStamina = Mathf.Min(GetAttribute("MaximumStamina"), GetAttribute("Stamina") + GetAttribute("StaminaRecoverPerAction"));
|
||
int remainingStamina = predictedStamina - currentIntention.guaranteedStamina;
|
||
int predictedMana = Mathf.Min(GetAttribute("MaximumMana"), GetAttribute("Mana") + GetAttribute("ManaRecoverPerAction"));
|
||
int remainingMana = predictedMana - currentIntention.guaranteedMana;
|
||
|
||
List<CardInstance> forced = new List<CardInstance>();
|
||
List<CardInstance> normal = new List<CardInstance>();
|
||
|
||
foreach (CardInstance card in availableCards)
|
||
{
|
||
if (card.weightSubmodule.forceUse)
|
||
{
|
||
forced.Add(card);
|
||
}
|
||
else
|
||
{
|
||
normal.Add(card);
|
||
}
|
||
}
|
||
|
||
intentionSubmodule.intendedCards.Clear();
|
||
//(characterView.hudContainer.enablingHUDs["Intention"] as Intention)?.Clear();
|
||
|
||
// 1. 优先处理强制选择卡牌
|
||
foreach (CardInstance card in forced)
|
||
{
|
||
if (currentIntention.maxCardCount > 0 && intended.Count >= currentIntention.maxCardCount)
|
||
{
|
||
break; // 已达数量上限
|
||
}
|
||
|
||
if (CanAfford(card, remainingStamina, remainingMana))
|
||
{
|
||
if(!CheckAvailabilityAndSetTargets(card, out List<CharacterBase> targets))
|
||
{
|
||
continue; // 无有效目标或无法使用则跳过
|
||
}
|
||
|
||
intended.Add(new IntendedCard(card, targets));
|
||
remainingStamina -= card.GetAttribute(CardAttributes.StaminaCost);
|
||
remainingMana -= card.GetAttribute(CardAttributes.ManaCost);
|
||
}
|
||
// 行动力不足则跳过该卡
|
||
}
|
||
|
||
// 2. 在剩余普通卡牌中基于权重随机选取
|
||
while (intended.Count < currentIntention.maxCardCount)
|
||
{
|
||
// 筛选出当前资源下还能出的牌
|
||
List<CardInstance> affordableCards = normal.FindAll(card => CanAfford(card, remainingStamina, remainingMana));
|
||
|
||
if (affordableCards.Count == 0)
|
||
{
|
||
break;
|
||
}
|
||
|
||
float totalWeight = affordableCards.Sum(card => card.weightSubmodule.currentWeight);
|
||
if (totalWeight <= 0f) break;
|
||
|
||
float r = Random.value * totalWeight;
|
||
float accum = 0f;
|
||
CardInstance chosen = null;
|
||
foreach (CardInstance card in affordableCards)
|
||
{
|
||
accum += card.weightSubmodule.currentWeight;
|
||
if (r <= accum)
|
||
{
|
||
chosen = card;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (chosen != null)
|
||
{
|
||
if (!CheckAvailabilityAndSetTargets(chosen, out List<CharacterBase> targets))
|
||
{
|
||
normal.Remove(chosen);
|
||
continue; // 无有效目标或无法使用则跳过
|
||
}
|
||
|
||
intended.Add(new IntendedCard(chosen, targets));
|
||
normal.Remove(chosen);
|
||
remainingStamina -= chosen.GetAttribute(CardAttributes.StaminaCost);
|
||
remainingMana -= chosen.GetAttribute(CardAttributes.ManaCost);
|
||
}
|
||
}
|
||
|
||
intentionSubmodule.intendedCards.AddRange(intended);
|
||
}
|
||
|
||
bool CanAfford(CardInstance card, int stamina, int mana)
|
||
{
|
||
return card.GetAttribute(CardAttributes.StaminaCost) <= stamina &&
|
||
card.GetAttribute(CardAttributes.ManaCost) <= mana;
|
||
}
|
||
|
||
public bool CheckAvailabilityAndSetTargets(CardInstance card, out List<CharacterBase> targets)
|
||
{
|
||
card.DetectTargetsValidity(out List<CharacterBase> valid, out _, out _);
|
||
if (valid.Count == 0)
|
||
{
|
||
targets = null;
|
||
return false; // 无有效目标或无法使用则跳过
|
||
}
|
||
|
||
targets = card.SetRandomTargets(valid);
|
||
|
||
return true;
|
||
}
|
||
}
|
||
} |