2026-06-07 20:23:45 +08:00

329 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* @Author: 白哉
* @Description: 勇仪推技能
* @Date: 2026年03月06日
* @Modify:
*/
using RuntimeData;
using System;
using System.Collections.Generic;
using System.Linq;
using MemoryPack;
using Logic.CrashSight;
using TH1_Anim;
using TH1_Logic.Core;
using UnityEngine;
namespace Logic.Skill
{
/// <summary>
/// YuugiPush 专用的本次攻击结果。普通攻击不使用这个对象。
/// 这里保存的是逻辑结算前后的关键格子,供 UnitAttackAction 在目标死亡、
/// 攻击者被反击杀死等情况下仍能播放正确的推击动画。
/// </summary>
public class YuugiPushResult
{
/// <summary>本次是否真的发生了推击位移。</summary>
public bool Pushed;
/// <summary>target 是否在被推到 TargetGridAfterPush 后被本次推击伤害击杀。</summary>
public bool TargetKilledAfterPush;
/// <summary>攻击开始前 self 所在格。</summary>
public GridData OriginGridBeforePush;
/// <summary>攻击开始前 target 所在格,也是 self 推击后前进到的格子。</summary>
public GridData TargetGridBeforePush;
/// <summary>target 被推到的格子target 死亡后数据层会移除单位,所以动画层必须缓存这个格子。</summary>
public GridData TargetGridAfterPush;
}
public partial class YuugiPushSkill : SkillBase
{
public YuugiPushSkill()
{
IsPermanent = true;
TurnsLimit = 0;
Score = 4;
}
public override SkillType GetSkillType()
{
return SkillType.YuugiPush;
}
/// <summary>
/// 勇仪推击的权威数据规则,动画由 UnitAttackAction.Execute 按 YuugiPushResult 播放。
///
/// 预期矩阵:
/// 1. canPush = true 且不致死:
/// target: targetGrid -> nextGridself: selfGrid -> targetGrid
/// 然后在移动后的格子上结算伤害。target 存活且满足反击规则时,从 nextGrid 反击。
/// 2. canPush = true 且致死:
/// 仍然必须先执行同样的推击位移target 死在 nextGridself 停在 targetGrid。
/// 动画层必须播放 target 后退、self 前进,然后在 nextGrid 播放死亡;
/// 绝不能退化成普通 MoveKill也不能因为 target 死亡后无 grid 而丢掉动画。
/// 注意:这两个位移是本次攻击内部的站位调整,不是独立移动行为;
/// 它们不触发 BeforeMove/OnMove/OnAnyUnitMove避免 STOMP、GridRadiation 等移动副作用
/// 抢在 PushAttack 伤害前杀死目标。但仍必须处理 BoatToLand/LandToBoat 形态合法化。
/// 3. canPush = false
/// 背后有单位、越界、地形/港口不合法等都会进入该分支;不发生任何推击位移,
/// 伤害使用基础伤害 +2。target 存活且满足反击规则时,按普通攻击反击。
/// 4. canPush = false 且致死:
/// self 能合法进入 targetGrid 时走普通 MoveKill不能进入时走 NotMoveKill。
/// 两者都必须清理 target renderer避免死亡单位贴图残留。
/// </summary>
/// <returns>true 表示推击规则已接管本次攻击false 表示不满足推击技能前置条件,应走普通攻击。</returns>
public bool YuugiPushAttack(UnitData self, UnitData target, MapData map,
out int outAttackDmg, out int outCounterDmg, out FragmentType outFragmentType, out YuugiPushResult outResult)
{
outAttackDmg = 0;
outCounterDmg = 0;
outFragmentType = FragmentType.Attack;
outResult = null;
if (self == null || target == null) return false;
var fullType = new UnitFullType();
fullType.UnitType = UnitType.BonePile;
if (self.GetAttackRange(map) > 1) return false;
if (self.GetAllAttackValue(map, target) <= target.GetAllDefenseValueIgnoringPositiveBonus(map, self)) return false;
var player = self.Player(map);
if (player == null) return false;
var targetPlayer = target.Player(map);
if (targetPlayer == null) return false;
map.GetCapitalCityDataByPlayerId(player.Id, out var city);
var targetCity = target.City(map);
if (city == null || targetCity == null) return false;
var selfGrid = self.Grid(map);
var targetGrid = target.Grid(map);
if (selfGrid == null || targetGrid == null) return false;
var nextGrid = map.GridMap.GetNextGrid(selfGrid, targetGrid);
var dmg = Table.Instance.CalcDamage(map, self, target);
// 判断能否推动
// 使用 CheckUnitAbleForGrid_OfflineStatus 而非 CheckLandTypeForGrid:
// 前者会对 LandAndPort 单位检查港口归属(必须同联盟),后者只看地形 LandType。
// 之前用纯 LandType 判定会让勇仪(LandAndPort)合法走进敌方港口。
// self 只需要能进入 targetGrid被推到 nextGrid 的是 target不要求 self 也能进入 nextGrid。
// 因此岸边推船只允许 targetGrid 是桥、陆地、我方/盟友港口等 self 合法格,不能隔着普通海格推船。
bool canPush = nextGrid != null && !nextGrid.RealUnit(map, out _)
&& Main.UnitLogic.CheckUnitAbleForGrid_RealTimeStatus(map, target, nextGrid)
&& Main.UnitLogic.CheckUnitAbleForGrid_OfflineStatus(map, player, self, targetGrid)
&& Main.UnitLogic.CheckUnitCanEnterGridByDiplomacyStatus(map, self, targetGrid);
// 能推→无额外伤害;不能推→+2伤害
int actualDmg = canPush ? dmg : dmg + 2;
if (canPush)
{
// 推击:无论是否击杀,都先执行 target 后退和 self 前进。是否死亡只决定后续攻击 fragment。
outResult = new YuugiPushResult
{
Pushed = true,
OriginGridBeforePush = selfGrid,
TargetGridBeforePush = targetGrid,
TargetGridAfterPush = nextGrid,
};
if (!YuugiPushRepositionWithoutMoveSideEffects(map, target, nextGrid))
{
LogSystem.LogError($"YuugiPush target move failed. self={self.Id}, target={target.Id}, from={targetGrid.Id}, to={nextGrid.Id}");
outResult = null;
return false;
}
if (!target.IsValidOnMap(map) || !target.IsAlive() || !map.GetGridDataByUnitId(target.Id, out var targetGridAfterMove) || targetGridAfterMove.Id != nextGrid.Id)
{
LogSystem.LogError($"YuugiPush aborted after target move. self={self.Id}, target={target.Id}, expectedTargetGrid={nextGrid.Id}");
outResult = null;
return true;
}
if (!self.IsValidOnMap(map) || !self.IsAlive())
{
LogSystem.LogError($"YuugiPush aborted: self invalid after target move. self={self.Id}, target={target.Id}");
outResult = null;
return true;
}
if (targetGrid.RealUnit(map, out var targetGridOccupant) && targetGridOccupant.Id != self.Id)
{
LogSystem.LogError($"YuugiPush aborted: target origin grid occupied after push. self={self.Id}, target={target.Id}, grid={targetGrid.Id}, occupant={targetGridOccupant.Id}");
outResult = null;
return true;
}
if (!YuugiPushRepositionWithoutMoveSideEffects(map, self, targetGrid))
{
LogSystem.LogError($"YuugiPush self move failed. self={self.Id}, target={target.Id}, to={targetGrid.Id}");
outResult = null;
return true;
}
if (!self.IsValidOnMap(map) || !self.IsAlive() || !map.GetGridDataByUnitId(self.Id, out var selfGridAfterMove) || selfGridAfterMove.Id != targetGrid.Id)
{
LogSystem.LogError($"YuugiPush aborted after self move. self={self.Id}, target={target.Id}, expectedSelfGrid={targetGrid.Id}");
outResult = null;
return true;
}
if (!target.IsValidOnMap(map) || !target.IsAlive() || !map.GetGridDataByUnitId(target.Id, out targetGridAfterMove) || targetGridAfterMove.Id != nextGrid.Id)
{
LogSystem.LogError($"YuugiPush aborted before damage. self={self.Id}, target={target.Id}, expectedTargetGrid={nextGrid.Id}");
outResult = null;
return true;
}
var cDmg = Table.Instance.CalcCounterDamage(map, self, target);
// 使用 push 专用反击判定:绕开通用 CanCounter 中 dmg1 重算导致的误判秒杀问题。
// 推击的伤害在移动前已经确定,反击只应在结算后 target 仍存活时判断;
// 其余合法限制(联盟/IsLimit*CounterAttack/视野/距离)仍然保留。
bool canCounter = CanCounterForPush(map, self, target);
outAttackDmg = dmg;
Main.UnitLogic.DamageSettlement(map, self, target, dmg, DamageType.PushAttack);
outResult.TargetKilledAfterPush = !target.IsAlive();
if (canCounter && target.IsAlive() && self.IsAlive())
{
outCounterDmg = cDmg;
Main.UnitLogic.DamageSettlement(map, target, self, cDmg, DamageType.CounterAttack);
outFragmentType = !self.IsAlive() ? FragmentType.AttackAndCounterDie : FragmentType.AttackAndCounter;
}
else if (!target.IsAlive())
{
outFragmentType = FragmentType.NotMoveKill;
TryCreateBonePile(map, player, city, targetCity, target, targetGrid, fullType);
}
}
else
{
// 不能推动、或伤害足以击杀:直接攻击(含可能的+2
outAttackDmg = actualDmg;
var cDmg = Table.Instance.CalcCounterDamage(map, self, target);
bool canCounter = Main.UnitLogic.CanCounterByRules(map, self, target);
Main.UnitLogic.DamageSettlement(map, self, target, actualDmg, DamageType.PushAttack);
if (target.IsAlive() && self.IsAlive() && canCounter)
{
// 未击杀:对方反击
outCounterDmg = cDmg;
Main.UnitLogic.DamageSettlement(map, target, self, cDmg, DamageType.CounterAttack);
outFragmentType = !self.IsAlive() ? FragmentType.AttackAndCounterDie : FragmentType.AttackAndCounter;
}
else if (!target.IsAlive() && Main.UnitLogic.CheckUnitAbleForGrid_RealTimeStatus(map, self, targetGrid))
{
outFragmentType = FragmentType.MoveKill;
Main.UnitLogic.MoveToLogic(map, self, targetGrid, MoveType.AttackMove);
TryCreateBonePile(map, player, city, targetCity, target, targetGrid, fullType);
}
else if (!target.IsAlive())
{
outFragmentType = FragmentType.NotMoveKill;
}
}
return true;
}
private static void TryCreateBonePile(MapData map, PlayerData player, CityData city, CityData targetCity, UnitData target, GridData targetGrid, UnitFullType fullType)
{
// 击杀英雄不生成骨堆。骨堆围绕被攻击者原格生成,保持旧逻辑位置,避免推击致死占用 nextGrid。
if (target.TreatedAsHero(map, target)) return;
if (!TryGetBonePileReviveType(target, out var reviveType)) return;
var aroundBuf = RentAroundBuf();
map.GridMap.GetAroundGridData(1, 1, targetGrid, aroundBuf);
var randomList = new List<GridData>();
foreach (var grid in aroundBuf)
{
if (grid == targetGrid) continue;
if (grid.RealUnit(map,out _)) continue;
if(!map.CheckLandTypeForGrid(fullType, grid))continue;
if(!map.CheckLandTypeForGrid(reviveType, grid))continue;
randomList.Add(grid);
}
ReturnAroundBuf();
if (randomList.Count <= 0) return;
var index = map.Net.GetRandom(map).Next(0, randomList.Count - 1);
if (!map.AddUnitData(randomList[index].Id, city.Id, fullType, out var bone)) return;
bone.GetSkill(SkillType.BonePile, out var skill);
var bonePile = skill as BonePileSkill;
if (bonePile != null)
{
bonePile.TargetType = reviveType;
bonePile.TargetCityId = targetCity.Id;
}
bone.Health = Mathf.Max(1, UnitData.CeilPositiveToInt(bone.GetMaxHealth() / 4f));
var boneGrid = bone.Grid(map);
if (boneGrid == null) return;
var sightRadius = boneGrid.Feature == TerrainFeature.Mountain ? 2 : 1;
Main.PlayerLogic.UpdateSightByRadius_LogicView(map, player, boneGrid, sightRadius);
}
private static bool TryGetBonePileReviveType(UnitData target, out UnitFullType reviveType)
{
reviveType = target.UnitFullType;
var targetLandType = target.GetLandType();
if (targetLandType is not (LandType.WaterAndAshore or LandType.WaterOnly)) return true;
// 船形态只是上水后的外壳,骨堆复生必须使用船里携带的原单位形态。
// 是否能落在具体骨堆格,由 TryCreateBonePile 的候选格筛选统一检查。
var carryType = target.CarryUnitFullType;
if (carryType.UnitType == UnitType.None) return false;
reviveType = carryType;
return true;
}
private static bool YuugiPushRepositionWithoutMoveSideEffects(MapData map, UnitData unit, GridData grid)
{
if (map == null || unit == null || grid == null) return false;
if (grid.RealUnit(map, out var occupant) && occupant.Id != unit.Id) return false;
if (unit.GetLandType() == LandType.WaterAndAshore && grid.Terrain == TerrainType.Land)
{
var carryType = unit.CarryUnitFullType;
if (carryType.UnitType == UnitType.None || !map.CheckLandTypeForGrid(carryType, grid)) return false;
}
map.SetUnitIdToGridId(unit.Id, grid.Id);
// YuugiPush 的位移属于同一次攻击的站位调整:不能触发移动技能副作用,
// 但必须保持水陆形态与最终格子一致,避免船停陆地或陆地形态停港口。
if (unit.GetLandType() == LandType.LandAndPort && grid.Resource == ResourceType.Port)
Main.UnitLogic.LandToBoat(map, unit);
else if (unit.GetLandType() == LandType.WaterAndAshore && grid.Terrain == TerrainType.Land)
Main.UnitLogic.BoatToLand(map, unit);
return true;
}
// Push 场景专用的反击判定。
// 问题背景:通用 UnitLogic.CanCounter 内部会重算 dmg1 = CalcDamage(self, target)
// push 发生后 self/target 的位置都变了,基于位置的 skill 加成可能让 dmg1 漂移;
// 如果漂移后 dmg1 >= target.HealthCanCounter 会判定"一击必杀 → 不反击"
// 但实际 DamageSettlement 用的还是 push 入口处算好的 dmg< target.Health
// target 其实没被秒杀 —— 结果就是"推开了、没死、但也不反击"的 bug。
// 修复思路push 入口条件 actualDmg < target.Health 已保证非秒杀,
// 所以这里跳过重算 + 秒杀检查,只保留其余合法限制。
private static bool CanCounterForPush(MapData map, UnitData self, UnitData target)
{
if (!target.CanAttackAll(map) && map.IsLeagueUnitByUnit(self.Id, target.Id)) return false;
if (!map.GetPlayerDataByUnitId(target.Id, out var targetPlayer)) return false;
if (!map.GetGridDataByUnitId(self.Id, out var selfGrid)) return false;
if (!map.GetGridDataByUnitId(target.Id, out var targetGrid)) return false;
if (self.IsLimitTargetCounterAttack(map)) return false;
if (target.IsLimitSelfCounterAttack(map)) return false;
// target 所属玩家必须能看见 self 新位置
if (!targetPlayer.Sight.CheckIsInSight(selfGrid.Id)) return false;
// target 的攻击范围必须能覆盖 self
var dist = Table.Instance.CalcDistance(
new Vector2Int(selfGrid.Pos.X, selfGrid.Pos.Y),
new Vector2Int(targetGrid.Pos.X, targetGrid.Pos.Y));
if (dist > target.GetAttackRange(map)) return false;
return true;
}
}
}