328 lines
17 KiB
C#
328 lines
17 KiB
C#
/*
|
||
* @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 -> nextGrid;self: selfGrid -> targetGrid;
|
||
/// 然后在移动后的格子上结算伤害。target 存活且满足反击规则时,从 nextGrid 反击。
|
||
/// 2. canPush = true 且致死:
|
||
/// 仍然必须先执行同样的推击位移;target 死在 nextGrid,self 停在 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_OfflineStatus(map, targetPlayer, target, nextGrid)
|
||
&& Main.UnitLogic.CheckUnitAbleForGrid_OfflineStatus(map, player, 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_OfflineStatus(map, player, 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.Health,CanCounter 会判定"一击必杀 → 不反击",
|
||
// 但实际 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;
|
||
}
|
||
}
|
||
}
|
||
|