143 lines
6.2 KiB
C#
143 lines
6.2 KiB
C#
using System.Threading;
|
||
using Ananta.SDK.Network;
|
||
using Ananta.SDK.Serialization;
|
||
using Ananta.Server.Protocol.Client4229938;
|
||
using GameMethods = Ananta.Server.RpcTypes.Client4229938.Methods.Game;
|
||
using SceneMethods = Ananta.Server.RpcTypes.Client4229938.Methods.GameScene;
|
||
|
||
namespace Ananta.Server.Handlers.Game;
|
||
|
||
/// <summary>
|
||
/// 敌人<b>血条</b>的显示时机 —— <c>SyncAddUnitState</c> / <c>SyncRemoveUnitState</c> 推
|
||
/// <c>UnitStateConfig.HideHp</c>。
|
||
///
|
||
/// ⛑⛑ 与 <c>GameRouter.EnemyWarning.cs</c>(detectionState 那条)是<b>两条完全不同的路</b>,
|
||
/// 别再混为一谈。客户端判据在 <c>EnemyHUDCtrl.lua</c>:
|
||
/// · <c>RefreshHpVisible</c>(:551) 决定血条 —— 看 <c>cfg.ShowHPBar</c>(模板配置,
|
||
/// 服务端改不了)和 <c>shouldShowHp</c> 里的 <c>UnitStateConfig.HideHp</c>(<b>我们能写</b>)。
|
||
/// · <c>detectionState</c> 只在 :85 绑到 <c>OnShowDetectionEffect</c>,动的是<b>潜行警戒条</b>,
|
||
/// 跟血条无关。所以警戒状态机从 Idle 推到 Fight 血条也不会变。
|
||
///
|
||
/// 结论:想让血条"不打不显示、打了才显示",唯一 per-unit 且服务端可写的开关就是 HideHp。
|
||
/// 先例:<c>GameRouter.HackerSummon.cs:341</c> 用同一招关掉机械狗的敌人血条。
|
||
/// </summary>
|
||
internal sealed partial class GameRouter
|
||
{
|
||
/// <summary>
|
||
/// <c>UnitStateConfig.HideHp</c> = 10266(dump.cs:1448623 / 1449004)。
|
||
/// ⛑ 不是 <c>UnitStateAtlasConfig</c> 的图集 id,也不是 DeadS(2)。
|
||
/// </summary>
|
||
private const uint LocalEnemyUnitStateHideHp = 10266;
|
||
|
||
/// <summary>spawn 批之后补发 HideHp 的延迟(ms)。与 <c>ReEngageSpawnedEnemyAsync</c> 的 1500 对齐。</summary>
|
||
private const int LocalEnemyHpBarHideReassertMs = 1500;
|
||
|
||
/// <summary>
|
||
/// spawn 之后调用:<c>SyncAddUnitState</c>(68026128) 把血条收起。
|
||
/// 幂等 —— 已经挂着 HideHp 的单位不会再发一次。
|
||
/// </summary>
|
||
internal static async Task HideLocalEnemyHpBarAsync(TcpSession session, ulong uid)
|
||
{
|
||
if (!EnemyHit.HideHpBarUntilHit)
|
||
return;
|
||
|
||
var runtime = GetLocalEnemyRuntimeState(session);
|
||
lock (runtime.SyncRoot)
|
||
{
|
||
if (!runtime.Enemies.TryGetValue(uid, out var existing) || existing is null) return;
|
||
// 只有 server-spawned 的走这套。client-spawned 的本地单位客户端自己建好了血条,
|
||
// 我们第一次知道它存在的时候它已经在挨打了,没机会提前收。
|
||
if (!existing.ServerSpawned || existing.Dead || existing.HpBarHidden) return;
|
||
}
|
||
|
||
try
|
||
{
|
||
await session.NotifyAsync(MethodId.SyncAddUnitState,
|
||
UxSerializer.Serialize(new GameMethods.SyncAddUnitStateArgs4229938
|
||
{
|
||
Id = uid,
|
||
State = LocalEnemyUnitStateHideHp,
|
||
EffectFreezeState = 0u,
|
||
}), CancellationToken.None);
|
||
|
||
bool hidden;
|
||
lock (runtime.SyncRoot)
|
||
{
|
||
hidden = runtime.Enemies.TryGetValue(uid, out var rec) && rec is not null;
|
||
if (hidden) rec!.HpBarHidden = true;
|
||
}
|
||
if (!hidden) return;
|
||
session.Log.Info($"[ENEMY-HPBAR] hidden uid={uid} state=HideHp({LocalEnemyUnitStateHideHp}) — bar shows on first hit");
|
||
|
||
// 补一拍。spawn 批是几十个包,AddUnitState 可能比单位在客户端建好更早到,
|
||
// 那就白发了(engage 那五件套也吃这个亏,ReEngageSpawnedEnemyAsync 同样延 1.5 s)。
|
||
// 若这 1.5 s 里玩家已经打中,HpBarHidden 已置 false ⇒ 这里跳过,不会把血条又收回去。
|
||
_ = ReassertHiddenHpBarAsync(session, uid);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
session.Log.Warn($"[ENEMY-HPBAR] hide failed uid={uid}: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
private static async Task ReassertHiddenHpBarAsync(TcpSession session, ulong uid)
|
||
{
|
||
try
|
||
{
|
||
await Task.Delay(LocalEnemyHpBarHideReassertMs, CancellationToken.None);
|
||
|
||
var runtime = GetLocalEnemyRuntimeState(session);
|
||
lock (runtime.SyncRoot)
|
||
{
|
||
if (!runtime.Enemies.TryGetValue(uid, out var rec) || rec is null) return;
|
||
if (!rec.HpBarHidden || rec.Dead) return;
|
||
}
|
||
|
||
await session.NotifyAsync(MethodId.SyncAddUnitState,
|
||
UxSerializer.Serialize(new GameMethods.SyncAddUnitStateArgs4229938
|
||
{
|
||
Id = uid,
|
||
State = LocalEnemyUnitStateHideHp,
|
||
EffectFreezeState = 0u,
|
||
}), CancellationToken.None);
|
||
session.Log.Info($"[ENEMY-HPBAR] reassert uid={uid} state=HideHp({LocalEnemyUnitStateHideHp})");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
session.Log.Warn($"[ENEMY-HPBAR] reassert failed uid={uid}: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 第一次命中时调用:<c>SyncRemoveUnitState</c>(68566559) 撤掉 HideHp,血条出现。
|
||
/// 只在 <c>HpBarHidden</c> 为真时发,之后自动变 no-op(不会每一下都刷包)。
|
||
/// </summary>
|
||
internal static async Task RevealLocalEnemyHpBarAsync(TcpSession session, ulong uid)
|
||
{
|
||
var runtime = GetLocalEnemyRuntimeState(session);
|
||
lock (runtime.SyncRoot)
|
||
{
|
||
if (!runtime.Enemies.TryGetValue(uid, out var existing) || existing is null) return;
|
||
if (!existing.HpBarHidden) return;
|
||
existing.HpBarHidden = false;
|
||
}
|
||
|
||
try
|
||
{
|
||
await session.NotifyAsync(MethodId.SyncRemoveUnitState,
|
||
UxSerializer.Serialize(new SceneMethods.SyncRemoveUnitState
|
||
{
|
||
unitId = uid,
|
||
state = LocalEnemyUnitStateHideHp,
|
||
reason = 1,
|
||
sourceId = 0u,
|
||
}), CancellationToken.None);
|
||
session.Log.Info($"[ENEMY-HPBAR] revealed uid={uid} state=HideHp({LocalEnemyUnitStateHideHp}) removed");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
session.Log.Warn($"[ENEMY-HPBAR] reveal failed uid={uid}: {ex.Message}");
|
||
}
|
||
}
|
||
}
|